sentiment情感分析代码注释

情感分析

第一步:导入豆瓣电影数据集,只有训练集和测试集

  • TorchText中的一个重要概念是FieldField决定了你的数据会被怎样处理。在我们的情感分类任务中,我们所需要接触到的数据有文本字符串和两种情感,”pos”或者”neg”。

  • Field的参数制定了数据会被怎样处理。

  • 我们使用TEXT field来定义如何处理电影评论,使用LABEL field来处理两个情感类别。

  • 我们的TEXT field带有tokenize='spacy',这表示我们会用spaCy tokenizer来tokenize英文句子。如果我们不特别声明tokenize这个参数,那么默认的分词方法是使用空格。

  • 安装spaCy

    1
    2
    pip install -U spacy
    python -m spacy download en
  • LABELLabelField定义。这是一种特别的用来处理label的Field。我们后面会解释dtype。

  • 更多关于Fields,参见https://github.com/pytorch/text/blob/master/torchtext/data/field.py

  • 和之前一样,我们会设定random seeds使实验可以复现。

  • TorchText支持很多常见的自然语言处理数据集。

  • 下面的代码会自动下载IMDb数据集,然后分成train/test两个torchtext.datasets类别。数据被前面的Fields处理。IMDb数据集一共有50000电影评论,每个评论都被标注为正面的或负面的。

先了解下Spacy库:spaCy介绍和使用教程
再了解下torchtext库:torchtext介绍和使用教程:这个新手必看,不看下面代码听不懂

In [ ]:

1
!ls

In [4]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import torch
from torchtext import data

SEED = 1234

torch.manual_seed(SEED) #为CPU设置随机种子
torch.cuda.manual_seed(SEED)#为GPU设置随机种子
torch.backends.cudnn.deterministic = True #在程序刚开始加这条语句可以提升一点训练速度,没什么额外开销。

#首先,我们要创建两个Field 对象:这两个对象包含了我们打算如何预处理文本数据的信息。
TEXT = data.Field(tokenize='spacy')
#torchtext.data.Field : 用来定义字段的处理方法(文本字段,标签字段)
# spaCy:英语分词器,类似于NLTK库,如果没有传递tokenize参数,则默认只是在空格上拆分字符串。
LABEL = data.LabelField(dtype=torch.float)
#LabelField是Field类的一个特殊子集,专门用于处理标签。

In [2]:

1
2
3
from torchtext import datasets
train_data, test_data = datasets.IMDB.splits(TEXT, LABEL)
# 加载豆瓣电影评论数据集
1
downloading aclImdb_v1.tar.gz
1
aclImdb_v1.tar.gz: 100%|██████████| 84.1M/84.1M [00:03<00:00, 22.8MB/s]

In [3]:

1
print(vars(train_data.examples[0])) #可以查看数据集长啥样子
1
{'text': ['This', 'movie', 'is', 'visually', 'stunning', '.', 'Who', 'cares', 'if', 'she', 'can', 'act', 'or', 'not', '.', 'Each', 'scene', 'is', 'a', 'work', 'of', 'art', 'composed', 'and', 'captured', 'by', 'John', 'Derek', '.', 'The', 'locations', ',', 'set', 'designs', ',', 'and', 'costumes', 'function', 'perfectly', 'to', 'convey', 'what', 'is', 'found', 'in', 'a', 'love', 'story', 'comprised', 'of', 'beauty', ',', 'youth', 'and', 'wealth', '.', 'In', 'some', 'ways', 'I', 'would', 'like', 'to', 'see', 'this', 'movie', 'as', 'a', 'tribute', 'to', 'John', 'and', 'Bo', 'Derek', "'s", 'story', '.', 'And', '...', 'this', 'commentary', 'would', 'not', 'be', 'complete', 'without', 'mentioning', 'Anthony', 'Quinn', "'s", 'role', 'as', 'father', ',', 'mentor', ',', 'lover', ',', 'and', 'his', 'portrayal', 'of', 'a', 'man', ',', 'of', 'men', ',', 'lost', 'to', 'a', 'bygone', 'era', 'when', 'men', 'were', 'men', '.', 'There', 'are', 'some', 'of', 'us', 'who', 'find', 'value', 'in', 'strength', 'and', 'direction', 'wrapped', 'in', 'a', 'confidence', 'that', 'contributes', 'to', 'a', 'sense', 'of', 'confidence', ',', 'containment', ',', 'and', 'security', '.', 'Yes', ',', 'they', 'do', 'not', 'make', 'men', 'like', 'that', 'anymore', '!', 'But', ',', 'then', 'how', 'often', 'do', 'you', 'find', 'women', 'who', 'are', 'made', 'like', 'Bo', 'Derek', '.'], 'label': 'pos'}

第二步:训练集划分为训练集和验证集

  • 由于我们现在只有train/test这两个分类,所以我们需要创建一个新的validation set。我们可以使用.split()创建新的分类。
  • 默认的数据分割是 70、30,如果我们声明split_ratio,可以改变split之间的比例,split_ratio=0.8表示80%的数据是训练集,20%是验证集。
  • 我们还声明random_state这个参数,确保我们每次分割的数据集都是一样的。

In [4]:

1
2
import random
train_data, valid_data = train_data.split(random_state=random.seed(SEED)) #默认split_ratio=0.7

In [5]:

1
2
3
print(f'Number of training examples: {len(train_data)}')
print(f'Number of validation examples: {len(valid_data)}')
print(f'Number of testing examples: {len(test_data)}')
1
2
3
Number of training examples: 17500
Number of validation examples: 7500
Number of testing examples: 25000

第三步:用训练集建立vocabulary,就是把每个单词一一映射到一个数字。

  • 下一步我们需要创建 vocabularyvocabulary 就是把每个单词一一映射到一个数字。img
  • 我们使用最常见的25k个单词来构建我们的单词表,用max_size这个参数可以做到这一点。
  • 所有其他的单词都用<unk>来表示。

In [6]:

1
2
3
4
5
6
7
# TEXT.build_vocab(train_data, max_size=25000)
# LABEL.build_vocab(train_data)
TEXT.build_vocab(train_data, max_size=25000, vectors="glove.6B.100d", unk_init=torch.Tensor.normal_)
#从预训练的词向量(vectors) 中,将当前(corpus语料库)词汇表的词向量抽取出来,构成当前 corpus 的 Vocab(词汇表)。
#预训练的 vectors 来自glove模型,每个单词有100维。glove模型训练的词向量参数来自很大的语料库,
#而我们的电影评论的语料库小一点,所以词向量需要更新,glove的词向量适合用做初始化参数。
LABEL.build_vocab(train_data)
1
2
.vector_cache/glove.6B.zip: 862MB [00:23, 36.0MB/s]                               
100%|█████████▉| 399597/400000 [00:25<00:00, 16569.01it/s]

In [7]:

1
2
print(f"Unique tokens in TEXT vocabulary: {len(TEXT.vocab)}")
print(f"Unique tokens in LABEL vocabulary: {len(LABEL.vocab)}")
1
2
Unique tokens in TEXT vocabulary: 25002
Unique tokens in LABEL vocabulary: 2

In [8]:

1
2
3
4
5
6
print(list(LABEL.vocab.stoi.items())) # 只有两个类别值
print(list(TEXT.vocab.stoi.items())[:20])
#语料库单词频率越高,索引越靠前。前两个默认为unk和pad。
print("------"*10)
print(TEXT.vocab.freqs.most_common(20))
# 这里可以看到unk和pad没有计数
1
2
3
4
[('neg', 0), ('pos', 1)]
[('<unk>', 0), ('<pad>', 1), ('the', 2), (',', 3), ('.', 4), ('a', 5), ('and', 6), ('of', 7), ('to', 8), ('is', 9), ('in', 10), ('I', 11), ('it', 12), ('that', 13), ('"', 14), ("'s", 15), ('this', 16), ('-', 17), ('/><br', 18), ('was', 19)]
------------------------------------------------------------
[('the', 201815), (',', 192511), ('.', 165127), ('a', 109096), ('and', 108875), ('of', 100402), ('to', 93905), ('is', 76001), ('in', 61097), ('I', 54439), ('it', 53649), ('that', 49325), ('"', 44431), ("'s", 43359), ('this', 42423), ('-', 37142), ('/><br', 35613), ('was', 34947), ('as', 30412), ('movie', 29873)]

In [9]:

1
print(TEXT.vocab.itos[:10]) #查看TEXT单词表
1
['<unk>', '<pad>', 'the', ',', '.', 'a', 'and', 'of', 'to', 'is']

第四步:创建iterators,每个itartion都会返回一个batch的样本。

  • 最后一步数据的准备是创建iterators。每个itartion都会返回一个batch的examples。
  • 我们会使用BucketIteratorBucketIterator会把长度差不多的句子放到同一个batch中,确保每个batch中不出现太多的padding。
  • 严格来说,我们这份notebook中的模型代码都有一个问题,也就是我们把<pad>也当做了模型的输入进行训练。更好的做法是在模型中把由<pad>产生的输出给消除掉。在这节课中我们简单处理,直接把<pad>也用作模型输入了。由于<pad>数量不多,模型的效果也不差。
  • 如果我们有GPU,还可以指定每个iteration返回的tensor都在GPU上。

In [11]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
BATCH_SIZE = 64

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

#相当于把样本划分batch,把相等长度的单词尽可能的划分到一个batch,不够长的就用padding。
train_iterator, valid_iterator, test_iterator = data.BucketIterator.splits(
(train_data, valid_data, test_data),
batch_size=BATCH_SIZE,
device=device)

'''
Iterator:标准迭代器

BucketIerator:相比于标准迭代器,会将类似长度的样本当做一批来处理,
因为在文本处理中经常会需要将每一批样本长度补齐为当前批中最长序列的长度,
因此当样本长度差别较大时,使用BucketIerator可以带来填充效率的提高。
除此之外,我们还可以在Field中通过fix_length参数来对样本进行截断补齐操作。

BPTTIterator: 基于BPTT(基于时间的反向传播算法)的迭代器,一般用于语言模型中。
'''

Out[11]:

1
'\nIterator:标准迭代器\n\nBucketIerator:相比于标准迭代器,会将类似长度的样本当做一批来处理,\n因为在文本处理中经常会需要将每一批样本长度补齐为当前批中最长序列的长度,\n因此当样本长度差别较大时,使用BucketIerator可以带来填充效率的提高。\n除此之外,我们还可以在Field中通过fix_length参数来对样本进行截断补齐操作。\n\nBPTTIterator: 基于BPTT(基于时间的反向传播算法)的迭代器,一般用于语言模型中。\n'

In [12]:

1
2
3
4
print(next(iter(train_iterator)).label.shape)
print(next(iter(train_iterator)).text.shape)#
# 多运行一次可以发现一条评论的单词长度会变
# 下面text的维度983*64,983为一条评论的单词长度
1
2
torch.Size([64])
torch.Size([983, 64])

In [13]:

1
2
3
4
5
# 取出一句评论
batch = next(iter(train_iterator))
print(batch.text.shape)
print([TEXT.vocab.itos[i] for i in batch.text[:,0]])
# 可以看到这句话的长度是1077,最后面有很多pad
1
2
torch.Size([1077, 64])
['It', 'was', 'interesting', 'to', 'see', 'how', 'accurate', 'the', 'writing', 'was', 'on', 'the', 'geek', 'buzz', 'words', ',', 'yet', 'very', 'naive', 'on', 'the', 'corporate', 'world', '.', 'The', 'Justice', 'Department', 'would', 'catch', 'more', 'of', 'the', 'big', '<unk>', 'giants', 'if', 'they', 'did', 'such', 'naive', 'things', 'to', 'win', '.', 'The', 'real', 'corporate', 'world', 'is', 'much', 'more', 'subtle', 'and', 'interesting', ',', 'yet', 'every', 'bit', 'as', 'sinister', '.', 'I', 'seriously', 'doubt', 'ANY', '<unk>', 'would', 'actually', 'kill', 'someone', 'directly', ';', 'even', 'the', '<unk>', 'is', 'more', '<unk>', 'these', 'days', '.', 'In', 'the', 'real', 'world', ',', 'they', 'do', 'kill', 'people', 'with', '<unk>', ',', 'pollution', ',', '<unk>', ',', '<unk>', ',', 'etc', '.', 'This', 'movie', 'must', 'have', 'been', 'developed', 'by', 'some', 'garage', 'geeks', ',', 'I', 'think', ',', 'and', 'the', 'studios', 'did', "n't", 'know', 'the', 'difference', '.', 'They', 'just', 'wanted', 'something', 'to', 'capitalize', 'on', 'the', 'Microsoft', '<unk>', 'case', 'in', 'the', 'news', '.', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>', '<pad>']

In [ ]:

1
2


第五步:创建Word Averaging模型

Word Averaging模型

  • 我们首先介绍一个简单的Word Averaging模型。这个模型非常简单,我们把每个单词都通过Embedding层投射成word embedding vector,然后把一句话中的所有word vector做个平均,就是整个句子的vector表示了。接下来把这个sentence vector传入一个Linear层,做分类即可。

img

  • 我们使用avg_pool2d来做average pooling。我们的目标是把sentence length那个维度平均成1,然后保留embedding这个维度。

img

  • avg_pool2d的kernel size是 (embedded.shape[1], 1),所以句子长度的那个维度会被压扁。

img

img

In [5]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import torch.nn as nn
import torch.nn.functional as F

class WordAVGModel(nn.Module):
def __init__(self, vocab_size, embedding_dim, output_dim, pad_idx):
#初始化参数,
super().__init__()
self.embedding = nn.Embedding(vocab_size, embedding_dim, padding_idx=pad_idx)
#vocab_size=词汇表长度=25002,embedding_dim=每个单词的维度=100
#padding_idx:如果提供的话,这里如果遇到padding的单词就用0填充。

self.fc = nn.Linear(embedding_dim, output_dim)
#output_dim输出的维度,一个数就可以了,=1

def forward(self, text):
# text.shape = (seq_len,batch_size)
# text下面会指定,为一个batch的数据,seq_len为一条评论的单词长度
embedded = self.embedding(text)
# embedded = [seq_len, batch_size, embedding_dim]
embedded = embedded.permute(1, 0, 2)
# [batch_size, seq_len, embedding_dim]更换顺序

pooled = F.avg_pool2d(embedded, (embedded.shape[1], 1)).squeeze(1)
# [batch size, embedding_dim] 把单词长度的维度压扁为1,并降维

return self.fc(pooled)
#(batch size, embedding_dim)*(embedding_dim, output_dim)=(batch size,output_dim)

In [6]:

1
2
3
4
5
6
7
8
INPUT_DIM = len(TEXT.vocab) #25002
EMBEDDING_DIM = 100
OUTPUT_DIM = 1 # 大于某个值是正,小于是负
PAD_IDX = TEXT.vocab.stoi[TEXT.pad_token]
# TEXT.pad_token = pad
# PAD_IDX = 1 为pad的索引

model = WordAVGModel(INPUT_DIM, EMBEDDING_DIM, OUTPUT_DIM, PAD_IDX)
1
2
3
4
5
6
7
8
9
10
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-6-d9889c88c56d> in <module>
----> 1 INPUT_DIM = len(TEXT.vocab) #25002
2 EMBEDDING_DIM = 100
3 OUTPUT_DIM = 1 # 大于某个值是正,小于是负
4 PAD_IDX = TEXT.vocab.stoi[TEXT.pad_token]
5 # TEXT.pad_token = pad

AttributeError: 'Field' object has no attribute 'vocab'

In [16]:

1
TEXT.pad_token

Out[16]:

1
'<pad>'

In [17]:

1
2
3
4
5
def count_parameters(model): #统计参数,可以不用管
return sum(p.numel() for p in model.parameters() if p.requires_grad)

print(f'The model has {count_parameters(model):,} trainable parameters')
# {}大括号里调用了函数
1
The model has 2,500,301 trainable parameters

第六步:初始化参数

In [18]:

1
2
3
4
# 把模型参数初始化成glove的向量参数
pretrained_embeddings = TEXT.vocab.vectors # 取出glove embedding词向量的参数
model.embedding.weight.data.copy_(pretrained_embeddings) #遇到_的语句直接替换,不需要另外赋值=
#把上面vectors="glove.6B.100d"取出的词向量作为初始化参数,数量为25000*100个参数

Out[18]:

1
2
3
4
5
6
7
tensor([[-0.1117, -0.4966,  0.1631,  ...,  1.2647, -0.2753, -0.1325],
[-0.8555, -0.7208, 1.3755, ..., 0.0825, -1.1314, 0.3997],
[-0.0382, -0.2449, 0.7281, ..., -0.1459, 0.8278, 0.2706],
...,
[-0.1419, 0.0282, 0.2185, ..., -0.1100, -0.1250, 0.0282],
[-0.3326, -0.9215, 0.9239, ..., 0.5057, -1.2898, 0.1782],
[-0.8304, 0.3732, 0.0726, ..., -0.0122, 0.2313, -0.2783]])

In [19]:

1
2
3
4
5
UNK_IDX = TEXT.vocab.stoi[TEXT.unk_token] # UNK_IDX=0

model.embedding.weight.data[UNK_IDX] = torch.zeros(EMBEDDING_DIM) #
model.embedding.weight.data[PAD_IDX] = torch.zeros(EMBEDDING_DIM)
#词汇表25002个单词,前两个unk和pad也需要初始化成EMBEDDING_DIM维的向量

第七步:训练模型

In [20]:

1
2
3
4
5
6
7
import torch.optim as optim

optimizer = optim.Adam(model.parameters()) #定义优化器
criterion = nn.BCEWithLogitsLoss() #定义损失函数,这个BCEWithLogitsLoss特殊情况,二分类损失函数
# nn.BCEWithLogitsLoss()看这个:https://blog.csdn.net/qq_22210253/article/details/85222093
model = model.to(device) #送到gpu上去
criterion = criterion.to(device) #送到gpu上去

计算预测的准确率

In [21]:

1
2
3
4
5
6
7
8
9
10
11
12
def binary_accuracy(preds, y): #计算准确率
"""
Returns accuracy per batch, i.e. if you get 8/10 right, this returns 0.8, NOT 8
"""

#round predictions to the closest integer
rounded_preds = torch.round(torch.sigmoid(preds))
#.round函数:四舍五入

correct = (rounded_preds == y).float() #convert into float for division
acc = correct.sum()/len(correct)
return acc

In [22]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
def train(model, iterator, optimizer, criterion):


epoch_loss = 0
epoch_acc = 0
total_len = 0
model.train() #model.train()代表了训练模式
#这步一定要加,是为了区分model训练和测试的模式的。
#有时候训练时会用到dropout、归一化等方法,但是测试的时候不能用dropout等方法。



for batch in iterator: #iterator为train_iterator
optimizer.zero_grad() #加这步防止梯度叠加

predictions = model(batch.text).squeeze(1)
#batch.text 就是上面forward函数的参数text
# squeeze(1)压缩维度,不然跟batch.label维度对不上

loss = criterion(predictions, batch.label)
acc = binary_accuracy(predictions, batch.label)
# 每次迭代都计算一边准确率


loss.backward() #反向传播
optimizer.step() #梯度下降

epoch_loss += loss.item() * len(batch.label)
#二分类损失函数loss因为已经平均化了,这里需要乘以len(batch.label),
#得到一个batch的损失,累加得到所有样本损失。

epoch_acc += acc.item() * len(batch.label)
#(acc.item():一个batch的正确率) *batch数 = 正确数
# 累加得到所有训练样本正确数。

total_len += len(batch.label)
#计算train_iterator所有样本的数量,不出意外应该是17500

return epoch_loss / total_len, epoch_acc / total_len
#epoch_loss / total_len :train_iterator所有batch的平均损失
#epoch_acc / total_len :train_iterator所有batch的平均正确率

In [23]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def evaluate(model, iterator, criterion):


epoch_loss = 0
epoch_acc = 0
total_len = 0

model.eval()
#转换成测试模式,冻结dropout层或其他层。

with torch.no_grad():
for batch in iterator:
#iterator为valid_iterator

#没有反向传播和梯度下降
predictions = model(batch.text).squeeze(1)
loss = criterion(predictions, batch.label)
acc = binary_accuracy(predictions, batch.label)


epoch_loss += loss.item() * len(batch.label)
epoch_acc += acc.item() * len(batch.label)
total_len += len(batch.label)
model.train() #调回训练模式

return epoch_loss / total_len, epoch_acc / total_len

In [24]:

1
2
3
4
5
6
7
import time 

def epoch_time(start_time, end_time): #查看每个epoch的时间
elapsed_time = end_time - start_time
elapsed_mins = int(elapsed_time / 60)
elapsed_secs = int(elapsed_time - (elapsed_mins * 60))
return elapsed_mins, elapsed_secs

第八步:查看模型运行结果

In [25]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 同上,这里用的kaggleGPU跑的,花了2分钟。
N_EPOCHS = 20

best_valid_loss = float('inf') #无穷大

for epoch in range(N_EPOCHS):

start_time = time.time()

train_loss, train_acc = train(model, train_iterator, optimizer, criterion)
# 得到训练集每个epoch的平均损失和准确率
valid_loss, valid_acc = evaluate(model, valid_iterator, criterion)
# 得到验证集每个epoch的平均损失和准确率,这个model里传入的参数是训练完的参数

end_time = time.time()

epoch_mins, epoch_secs = epoch_time(start_time, end_time)

if valid_loss < best_valid_loss: #只要模型效果变好,就存模型
best_valid_loss = valid_loss
torch.save(model.state_dict(), 'wordavg-model.pt')

print(f'Epoch: {epoch+1:02} | Epoch Time: {epoch_mins}m {epoch_secs}s')
print(f'\tTrain Loss: {train_loss:.3f} | Train Acc: {train_acc*100:.2f}%')
print(f'\t Val. Loss: {valid_loss:.3f} | Val. Acc: {valid_acc*100:.2f}%')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
Epoch: 01 | Epoch Time: 0m 5s
Train Loss: 0.684 | Train Acc: 58.78%
Val. Loss: 0.617 | Val. Acc: 72.51%
Epoch: 02 | Epoch Time: 0m 5s
Train Loss: 0.642 | Train Acc: 72.62%
Val. Loss: 0.504 | Val. Acc: 76.65%
Epoch: 03 | Epoch Time: 0m 5s
Train Loss: 0.569 | Train Acc: 78.81%
Val. Loss: 0.439 | Val. Acc: 81.07%
Epoch: 04 | Epoch Time: 0m 5s
Train Loss: 0.497 | Train Acc: 82.97%
Val. Loss: 0.404 | Val. Acc: 84.03%
Epoch: 05 | Epoch Time: 0m 5s
Train Loss: 0.435 | Train Acc: 85.95%
Val. Loss: 0.400 | Val. Acc: 85.69%
Epoch: 06 | Epoch Time: 0m 5s
Train Loss: 0.388 | Train Acc: 87.73%
Val. Loss: 0.412 | Val. Acc: 86.80%
Epoch: 07 | Epoch Time: 0m 5s
Train Loss: 0.349 | Train Acc: 88.83%
Val. Loss: 0.425 | Val. Acc: 87.64%
Epoch: 08 | Epoch Time: 0m 5s
Train Loss: 0.319 | Train Acc: 89.84%
Val. Loss: 0.446 | Val. Acc: 87.83%
Epoch: 09 | Epoch Time: 0m 5s
Train Loss: 0.293 | Train Acc: 90.54%
Val. Loss: 0.464 | Val. Acc: 88.25%
Epoch: 10 | Epoch Time: 0m 5s
Train Loss: 0.272 | Train Acc: 91.19%
Val. Loss: 0.480 | Val. Acc: 88.68%
Epoch: 11 | Epoch Time: 0m 5s
Train Loss: 0.254 | Train Acc: 91.82%
Val. Loss: 0.498 | Val. Acc: 88.87%
Epoch: 12 | Epoch Time: 0m 5s
Train Loss: 0.238 | Train Acc: 92.53%
Val. Loss: 0.517 | Val. Acc: 89.01%
Epoch: 13 | Epoch Time: 0m 5s
Train Loss: 0.222 | Train Acc: 93.03%
Val. Loss: 0.532 | Val. Acc: 89.25%
Epoch: 14 | Epoch Time: 0m 5s
Train Loss: 0.210 | Train Acc: 93.47%
Val. Loss: 0.547 | Val. Acc: 89.44%
Epoch: 15 | Epoch Time: 0m 5s
Train Loss: 0.198 | Train Acc: 93.95%
Val. Loss: 0.564 | Val. Acc: 89.49%
Epoch: 16 | Epoch Time: 0m 5s
Train Loss: 0.186 | Train Acc: 94.31%
Val. Loss: 0.582 | Val. Acc: 89.68%
Epoch: 17 | Epoch Time: 0m 5s
Train Loss: 0.175 | Train Acc: 94.74%
Val. Loss: 0.596 | Val. Acc: 89.69%
Epoch: 18 | Epoch Time: 0m 5s
Train Loss: 0.166 | Train Acc: 95.09%
Val. Loss: 0.615 | Val. Acc: 89.95%
Epoch: 19 | Epoch Time: 0m 5s
Train Loss: 0.156 | Train Acc: 95.36%
Val. Loss: 0.631 | Val. Acc: 89.91%
Epoch: 20 | Epoch Time: 0m 5s
Train Loss: 0.147 | Train Acc: 95.75%
Val. Loss: 0.647 | Val. Acc: 90.07%

第九步:预测结果

In [26]:

1
!ls
1
__notebook_source__.ipynb  wordavg-model.pt

In [55]:

1
2
3
4
5
6
7
8
9
10
11
12
# kaggle上下载模型文件到本地,运行下面代码,点击输出的链接就行
from IPython.display import HTML
import pandas as pd
import numpy as np

def create_download_link(title = "Download model file", filename = "CNN-model.pt"):
html = '<a href={filename}>{title}</a>'
html = html.format(title=title,filename=filename)
return HTML(html)

# create a link to download the dataframe which was saved with .to_csv method
create_download_link(filename='wordavg-model.pt')

Out[55]:

Download model file

In [1]:

1
2
model.load_state_dict(torch.load("wordavg-model.pt"))
#用保存的模型参数预测数据
1
2
3
4
5
6
7
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
<ipython-input-1-f795a3e78d6a> in <module>
----> 1 model.load_state_dict(torch.load("wordavg-model.pt"))
2 #用保存的模型参数预测数据

NameError: name 'model' is not defined

In [28]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import spacy  #分词工具,跟NLTK类似
nlp = spacy.load('en')

def predict_sentiment(sentence): # 传入预测的句子I love This film bad
tokenized = [tok.text for tok in nlp.tokenizer(sentence)] #分词
# print(tokenized) = ['I', 'love', 'This', 'film', 'bad']
indexed = [TEXT.vocab.stoi[t] for t in tokenized]
#sentence的在25002中的索引

tensor = torch.LongTensor(indexed).to(device) #seq_len
# 所有词向量都应该变成LongTensor

tensor = tensor.unsqueeze(1)
#模型的输入是默认有batch_size的,需要升维,seq_len * batch_size(1)

prediction = torch.sigmoid(model(tensor))
# 预测准确率,在0,1之间,需要sigmoid下

return prediction.item()

In [29]:

1
predict_sentiment("I love This film bad")

Out[29]:

1
0.9373546242713928

In [30]:

1
predict_sentiment("This film is great")

Out[30]:

1
1.0

RNN模型

  • 下面我们尝试把模型换成一个

    recurrent neural network

(RNN)。RNN经常会被用来encode一个sequence

ℎ𝑡=RNN(𝑥𝑡,ℎ𝑡−1)ht=RNN(xt,ht−1)

  • 我们使用最后一个hidden state ℎ𝑇hT来表示整个句子。

  • 然后我们把ℎ𝑇hT通过一个线性变换𝑓f,然后用来预测句子的情感。

In [ ]:

1
2


In [32]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class RNN(nn.Module):
def __init__(self, vocab_size, embedding_dim, hidden_dim, output_dim,
n_layers, bidirectional, dropout, pad_idx):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embedding_dim, padding_idx=pad_idx)
self.rnn = nn.LSTM(embedding_dim, hidden_dim, num_layers=n_layers,
bidirectional=bidirectional, dropout=dropout)
#embedding_dim:每个单词维度
#hidden_dim:隐藏层维度
#num_layers:神经网络深度,纵向深度
#bidirectional:是否双向循环RNN
#这个自己先得理解LSTM各个维度,不然容易晕,双向RNN网络图示看上面,可以借鉴下


self.fc = nn.Linear(hidden_dim*2, output_dim)
# 这里hidden_dim乘以2是因为是双向,需要拼接两个方向,跟n_layers的层数无关。

self.dropout = nn.Dropout(dropout)

def forward(self, text):
# text.shape=[seq_len, batch_size]
embedded = self.dropout(self.embedding(text)) #[seq_len, batch_size, emb_dim]
output, (hidden, cell) = self.rnn(embedded)
# output = [seq_len, batch size, hid_dim * num directions]
# hidden = [num layers * num directions, batch_size, hid_dim]
# cell = [num layers * num directions, batch_size, hid_dim]
# 这里的num layers * num directions可以看上面图,上面图除掉输入输出层只有两层双向网络。
# num layers = 2表示需要纵向上在加两层双向,总共有4层神经元。
# 对于LSTM模型的任意一个时间序列t,h层的输出维度


#concat the final forward (hidden[-2,:,:]) and backward (hidden[-1,:,:]) hidden layers
#and apply dropout
hidden = self.dropout(torch.cat((hidden[-2,:,:], hidden[-1,:,:]), dim=1))
# hidden = [batch size, hid dim * num directions],
# 看下上面图示,最后前向和后向输出的隐藏层会concat到输出层,4层神经元最后两层作为最终的输出。
# 这里因为我们只需要得到最后一个时间序列的输出,所以最终输出的hidden跟seq_len无关。

return self.fc(hidden.squeeze(0)) # 在接一个全连接层,最终输出[batch size, output_dim]

In [ ]:

1
2


In [36]:

1
2
3
4
5
6
7
8
9
10
11
12
INPUT_DIM = len(TEXT.vocab)
EMBEDDING_DIM = 100
HIDDEN_DIM = 256
OUTPUT_DIM = 1
N_LAYERS = 2
BIDIRECTIONAL = True
DROPOUT = 0.5
PAD_IDX = TEXT.vocab.stoi[TEXT.pad_token]

model = RNN(INPUT_DIM, EMBEDDING_DIM, HIDDEN_DIM, OUTPUT_DIM,
N_LAYERS, BIDIRECTIONAL, DROPOUT, PAD_IDX)
model

Out[36]:

1
2
3
4
5
6
RNN(
(embedding): Embedding(25002, 100, padding_idx=1)
(rnn): LSTM(100, 256, num_layers=2, dropout=0.5, bidirectional=True)
(fc): Linear(in_features=512, out_features=1, bias=True)
(dropout): Dropout(p=0.5)
)

In [34]:

1
2
print(f'The model has {count_parameters(model):,} trainable parameters')
# 比averge model模型多了一倍的参数
1
The model has 4,810,857 trainable parameters

In [37]:

1
2
3
4
5
6
7
8
# 同上初始化
model.embedding.weight.data.copy_(pretrained_embeddings)
UNK_IDX = TEXT.vocab.stoi[TEXT.unk_token]

model.embedding.weight.data[UNK_IDX] = torch.zeros(EMBEDDING_DIM)
model.embedding.weight.data[PAD_IDX] = torch.zeros(EMBEDDING_DIM)

print(model.embedding.weight.data)
1
2
3
4
5
6
7
tensor([[ 0.0000,  0.0000,  0.0000,  ...,  0.0000,  0.0000,  0.0000],
[ 0.0000, 0.0000, 0.0000, ..., 0.0000, 0.0000, 0.0000],
[-0.0382, -0.2449, 0.7281, ..., -0.1459, 0.8278, 0.2706],
...,
[-0.1419, 0.0282, 0.2185, ..., -0.1100, -0.1250, 0.0282],
[-0.3326, -0.9215, 0.9239, ..., 0.5057, -1.2898, 0.1782],
[-0.8304, 0.3732, 0.0726, ..., -0.0122, 0.2313, -0.2783]])

训练RNN模型

In [38]:

1
2
optimizer = optim.Adam(model.parameters())
model = model.to(device)

In [39]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 同上,这里用的kaggleGPU跑的,花了40分钟。
N_EPOCHS = 20
best_valid_loss = float('inf')
for epoch in range(N_EPOCHS):
start_time = time.time()
train_loss, train_acc = train(model, train_iterator, optimizer, criterion)
valid_loss, valid_acc = evaluate(model, valid_iterator, criterion)

end_time = time.time()

epoch_mins, epoch_secs = epoch_time(start_time, end_time)

if valid_loss < best_valid_loss:
best_valid_loss = valid_loss
torch.save(model.state_dict(), 'lstm-model.pt')

print(f'Epoch: {epoch+1:02} | Epoch Time: {epoch_mins}m {epoch_secs}s')
print(f'\tTrain Loss: {train_loss:.3f} | Train Acc: {train_acc*100:.2f}%')
print(f'\t Val. Loss: {valid_loss:.3f} | Val. Acc: {valid_acc*100:.2f}%')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
Epoch: 01 | Epoch Time: 2m 1s
Train Loss: 0.667 | Train Acc: 59.09%
Val. Loss: 0.633 | Val. Acc: 64.67%
Epoch: 02 | Epoch Time: 2m 1s
Train Loss: 0.663 | Train Acc: 60.33%
Val. Loss: 0.669 | Val. Acc: 69.21%
Epoch: 03 | Epoch Time: 2m 2s
Train Loss: 0.650 | Train Acc: 61.06%
Val. Loss: 0.579 | Val. Acc: 70.55%
Epoch: 04 | Epoch Time: 2m 2s
Train Loss: 0.493 | Train Acc: 77.43%
Val. Loss: 0.382 | Val. Acc: 83.43%
Epoch: 05 | Epoch Time: 2m 2s
Train Loss: 0.394 | Train Acc: 83.71%
Val. Loss: 0.338 | Val. Acc: 85.97%
Epoch: 06 | Epoch Time: 2m 3s
Train Loss: 0.338 | Train Acc: 86.26%
Val. Loss: 0.309 | Val. Acc: 87.21%
Epoch: 07 | Epoch Time: 2m 2s
Train Loss: 0.292 | Train Acc: 88.37%
Val. Loss: 0.295 | Val. Acc: 88.73%
Epoch: 08 | Epoch Time: 2m 3s
Train Loss: 0.252 | Train Acc: 90.26%
Val. Loss: 0.300 | Val. Acc: 89.31%
Epoch: 09 | Epoch Time: 2m 2s
Train Loss: 0.246 | Train Acc: 90.51%
Val. Loss: 0.282 | Val. Acc: 88.76%
Epoch: 10 | Epoch Time: 2m 3s
Train Loss: 0.205 | Train Acc: 92.37%
Val. Loss: 0.295 | Val. Acc: 88.31%
Epoch: 11 | Epoch Time: 2m 1s
Train Loss: 0.203 | Train Acc: 92.46%
Val. Loss: 0.289 | Val. Acc: 89.25%
Epoch: 12 | Epoch Time: 2m 3s
Train Loss: 0.178 | Train Acc: 93.58%
Val. Loss: 0.301 | Val. Acc: 89.41%
Epoch: 13 | Epoch Time: 2m 3s
Train Loss: 0.158 | Train Acc: 94.43%
Val. Loss: 0.301 | Val. Acc: 89.51%
Epoch: 14 | Epoch Time: 2m 2s
Train Loss: 0.158 | Train Acc: 94.63%
Val. Loss: 0.289 | Val. Acc: 89.95%
Epoch: 15 | Epoch Time: 2m 2s
Train Loss: 0.142 | Train Acc: 95.00%
Val. Loss: 0.314 | Val. Acc: 89.59%
Epoch: 16 | Epoch Time: 2m 2s
Train Loss: 0.123 | Train Acc: 95.62%
Val. Loss: 0.329 | Val. Acc: 89.99%
Epoch: 17 | Epoch Time: 2m 4s
Train Loss: 0.107 | Train Acc: 96.16%
Val. Loss: 0.325 | Val. Acc: 89.75%
Epoch: 18 | Epoch Time: 2m 4s
Train Loss: 0.100 | Train Acc: 96.66%
Val. Loss: 0.341 | Val. Acc: 89.49%
Epoch: 19 | Epoch Time: 2m 3s
Train Loss: 0.096 | Train Acc: 96.63%
Val. Loss: 0.340 | Val. Acc: 89.79%
Epoch: 20 | Epoch Time: 2m 3s
Train Loss: 0.080 | Train Acc: 97.31%
Val. Loss: 0.380 | Val. Acc: 89.83%

You may have noticed the loss is not really decreasing and the accuracy is poor. This is due to several issues with the model which we’ll improve in the next notebook.

Finally, the metric we actually care about, the test loss and accuracy, which we get from our parameters that gave us the best validation loss.

In [40]:

1
2
3
4
5
6
7
8
9
10
11
12
# 下载文件到本地
from IPython.display import HTML
import pandas as pd
import numpy as np

def create_download_link(title = "Download model file", filename = "wordavg-model.pt"):
html = '<a href={filename}>{title}</a>'
html = html.format(title=title,filename=filename)
return HTML(html)

# create a link to download the dataframe which was saved with .to_csv method
create_download_link(filename='lstm-model.pt')

Out[40]:

Download model file

In [41]:

1
2
3
model.load_state_dict(torch.load('lstm-model.pt'))
test_loss, test_acc = evaluate(model, test_iterator, criterion)
print(f'Test Loss: {test_loss:.3f} | Test Acc: {test_acc*100:.2f}%')
1
Test Loss: 0.304 | Test Acc: 88.11%

In [44]:

1
predict_sentiment("I feel This film bad")

Out[44]:

1
0.3637591600418091

In [43]:

1
predict_sentiment("This film is great")

Out[43]:

1
0.9947803020477295

CNN模型

In [45]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class CNN(nn.Module):
def __init__(self, vocab_size, embedding_dim, n_filters,
filter_sizes, output_dim, dropout, pad_idx):
super().__init__()

self.embedding = nn.Embedding(vocab_size, embedding_dim, padding_idx=pad_idx)
self.convs = nn.ModuleList([
nn.Conv2d(in_channels = 1, out_channels = n_filters,
kernel_size = (fs, embedding_dim))
for fs in filter_sizes
])
# in_channels:输入的channel,文字都是1
# out_channels:输出的channel维度
# fs:每次滑动窗口计算用到几个单词
# for fs in filter_sizes打算用好几个卷积模型最后concate起来看效果。

self.fc = nn.Linear(len(filter_sizes) * n_filters, output_dim)
self.dropout = nn.Dropout(dropout)

def forward(self, text):
text = text.permute(1, 0) # [batch size, sent len]
embedded = self.embedding(text) # [batch size, sent len, emb dim]
embedded = embedded.unsqueeze(1) # [batch size, 1, sent len, emb dim]
# 升维是为了和nn.Conv2d的输入维度吻合,把channel列升维。
conved = [F.relu(conv(embedded)).squeeze(3) for conv in self.convs]
# conved = [batch size, n_filters, sent len - filter_sizes+1]
# 有几个filter_sizes就有几个conved


pooled = [F.max_pool1d(conv, conv.shape[2]).squeeze(2) for conv in conved]
# 把conv的第三个维度最大池化了
#pooled_n = [batch size, n_filters]

cat = self.dropout(torch.cat(pooled, dim=1))
# cat = [batch size, n_filters * len(filter_sizes)]
# 把 len(filter_sizes)个卷积模型concate起来传到全连接层。

return self.fc(cat)

In [47]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 同上
INPUT_DIM = len(TEXT.vocab)
EMBEDDING_DIM = 100
N_FILTERS = 100
FILTER_SIZES = [3,4,5]
OUTPUT_DIM = 1
DROPOUT = 0.5
PAD_IDX = TEXT.vocab.stoi[TEXT.pad_token]


model = CNN(INPUT_DIM, EMBEDDING_DIM, N_FILTERS, FILTER_SIZES, OUTPUT_DIM, DROPOUT, PAD_IDX)
model.embedding.weight.data.copy_(pretrained_embeddings)
UNK_IDX = TEXT.vocab.stoi[TEXT.unk_token]

model.embedding.weight.data[UNK_IDX] = torch.zeros(EMBEDDING_DIM)
model.embedding.weight.data[PAD_IDX] = torch.zeros(EMBEDDING_DIM)
model = model.to(device)
print(f'The model has {count_parameters(model):,} trainable parameters')
# 比averge model模型参数差不多
1
The model has 2,620,801 trainable parameters

In [48]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 同上,需要花8分钟左右
optimizer = optim.Adam(model.parameters())
criterion = nn.BCEWithLogitsLoss()
criterion = criterion.to(device)

N_EPOCHS = 20

best_valid_loss = float('inf')

for epoch in range(N_EPOCHS):

start_time = time.time()

train_loss, train_acc = train(model, train_iterator, optimizer, criterion)
valid_loss, valid_acc = evaluate(model, valid_iterator, criterion)

end_time = time.time()

epoch_mins, epoch_secs = epoch_time(start_time, end_time)

if valid_loss < best_valid_loss:
best_valid_loss = valid_loss
torch.save(model.state_dict(), 'CNN-model.pt')

print(f'Epoch: {epoch+1:02} | Epoch Time: {epoch_mins}m {epoch_secs}s')
print(f'\tTrain Loss: {train_loss:.3f} | Train Acc: {train_acc*100:.2f}%')
print(f'\t Val. Loss: {valid_loss:.3f} | Val. Acc: {valid_acc*100:.2f}%')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
Epoch: 01 | Epoch Time: 0m 19s
Train Loss: 0.652 | Train Acc: 61.81%
Val. Loss: 0.527 | Val. Acc: 76.20%
Epoch: 02 | Epoch Time: 0m 19s
Train Loss: 0.427 | Train Acc: 80.66%
Val. Loss: 0.358 | Val. Acc: 84.36%
Epoch: 03 | Epoch Time: 0m 19s
Train Loss: 0.304 | Train Acc: 87.14%
Val. Loss: 0.318 | Val. Acc: 86.45%
Epoch: 04 | Epoch Time: 0m 19s
Train Loss: 0.215 | Train Acc: 91.42%
Val. Loss: 0.313 | Val. Acc: 86.92%
Epoch: 05 | Epoch Time: 0m 19s
Train Loss: 0.156 | Train Acc: 94.18%
Val. Loss: 0.326 | Val. Acc: 87.01%
Epoch: 06 | Epoch Time: 0m 19s
Train Loss: 0.105 | Train Acc: 96.33%
Val. Loss: 0.344 | Val. Acc: 87.16%
Epoch: 07 | Epoch Time: 0m 19s
Train Loss: 0.075 | Train Acc: 97.61%
Val. Loss: 0.372 | Val. Acc: 87.28%
Epoch: 08 | Epoch Time: 0m 19s
Train Loss: 0.052 | Train Acc: 98.39%
Val. Loss: 0.403 | Val. Acc: 87.21%
Epoch: 09 | Epoch Time: 0m 19s
Train Loss: 0.041 | Train Acc: 98.64%
Val. Loss: 0.433 | Val. Acc: 87.09%
Epoch: 10 | Epoch Time: 0m 19s
Train Loss: 0.031 | Train Acc: 99.10%
Val. Loss: 0.462 | Val. Acc: 87.01%
Epoch: 11 | Epoch Time: 0m 19s
Train Loss: 0.023 | Train Acc: 99.29%
Val. Loss: 0.495 | Val. Acc: 86.93%
Epoch: 12 | Epoch Time: 0m 19s
Train Loss: 0.021 | Train Acc: 99.34%
Val. Loss: 0.530 | Val. Acc: 86.84%
Epoch: 13 | Epoch Time: 0m 19s
Train Loss: 0.015 | Train Acc: 99.60%
Val. Loss: 0.559 | Val. Acc: 86.73%
Epoch: 14 | Epoch Time: 0m 19s
Train Loss: 0.013 | Train Acc: 99.69%
Val. Loss: 0.597 | Val. Acc: 86.48%
Epoch: 15 | Epoch Time: 0m 19s
Train Loss: 0.012 | Train Acc: 99.70%
Val. Loss: 0.608 | Val. Acc: 86.63%
Epoch: 16 | Epoch Time: 0m 19s
Train Loss: 0.009 | Train Acc: 99.76%
Val. Loss: 0.640 | Val. Acc: 86.77%
Epoch: 17 | Epoch Time: 0m 19s
Train Loss: 0.010 | Train Acc: 99.73%
Val. Loss: 0.674 | Val. Acc: 86.51%
Epoch: 18 | Epoch Time: 0m 19s
Train Loss: 0.012 | Train Acc: 99.63%
Val. Loss: 0.704 | Val. Acc: 86.71%
Epoch: 19 | Epoch Time: 0m 19s
Train Loss: 0.010 | Train Acc: 99.65%
Val. Loss: 0.757 | Val. Acc: 86.44%
Epoch: 20 | Epoch Time: 0m 20s
Train Loss: 0.006 | Train Acc: 99.80%
Val. Loss: 0.756 | Val. Acc: 86.55%

In [49]:

1
2
3
4
# 发现上面结果过拟合了,同学们可以自行调参
model.load_state_dict(torch.load('CNN-model.pt'))
test_loss, test_acc = evaluate(model, test_iterator, criterion)
print(f'Test Loss: {test_loss:.3f} | Test Acc: {test_acc*100:.2f}%')
1
Test Loss: 0.339 | Test Acc: 85.68%

In [50]:

1
predict_sentiment("I feel This film bad")

Out[50]:

1
0.6535547375679016

In [52]:

1
2
predict_sentiment("This film is great well") 
# 我后面加了个well,不加会报错,因为我们的FILTER_SIZES = [3,4,5]有设置为5,所以输出的句子长度不能小于5

Out[52]:

1
0.9950380921363831

In [54]:

1
2
3
4
5
6
7
8
9
10
11
12
# kaggle上下载模型文件到本地
from IPython.display import HTML
import pandas as pd
import numpy as np

def create_download_link(title = "Download model file", filename = "CNN-model.pt"):
html = '<a href={filename}>{title}</a>'
html = html.format(title=title,filename=filename)
return HTML(html)

# create a link to download the dataframe which was saved with .to_csv method
create_download_link(filename='CNN-model.pt')

Out[54]:

Download model file

In [ ]:

1
 

聊天机器人二

概述

比较著名的聊天系统

img

img

聊天机器人的历史

  • 1950: Turing Test

  • 1966: ELIZA, MIT chatbot

  • 1995: ALICE, pattern recognition chatbot

  • 2011-2012: Siri, Watson, Google Assistant

  • 2015: Amazon Alexa, Microsoft Cortana

  • 2016: Bot 元年: Microsoft Bot Framework, FB Messenger, Google Assistant …

  • 2017: 百度度秘,腾讯云小微,阿里小蜜 …

ASR: Acoustic speech recognition: Speech –> Text

SLU: Spoken Language Understanding

img

聊天机器人按照其功能主要可以分为两类

  • 任务导向的聊天机器人(Task-Oriented Chatbot)

  • 有具体的聊天任务,例如酒店、机票、饭店预订,电话目录

  • 也可以做一些更复杂的工作,例如假期日程安排,讨价还价

  • goal-oriented chatbot

  • 非任务导向的聊天机器人(Non-Task-Oriented Chatbot)

  • 没有具体的聊天目标,主要目的是闲聊,能够和用户有更多的交互

  • 也有上述任务导向与非任务导向的混合聊天机器人,同时具备两种功能

按照聊天机器人的聊天领域 (Domain)

  • 对于任务导向的机器人,聊天的领域就是任务的领域

  • 关于某个特定任务的数据库,例如机票信息,酒店信息

  • 也可以同时包含多个领域的信息

  • 对于非任务导向的机器人

按照聊天的发起方

  • 系统主导(System Initiative)的聊天机器人

  • 适用于一些简单的任务:例如很多大公司的自动电话语音系统

  • 用户主导(User Initiative)的聊天机器人

  • 用户主导聊天的主题

  • 系统需要去尽量迎合用户的喜好,陪他们聊天

  • 系统/用户混合主导(Mixed Initiative)

Alexa Prize Challenge 亚马逊举办的聊天机器人大赛

https://developer.amazon.com/alexaprize/challenges/past-challenges/2018/

DSTC 2 & 3

关于构建聊天机器人的建议

  • 迅速创建一个baseline,在这个Baseline的基础上去不断提高

  • 多测试自己的聊天机器人

聊天机器人的评估方法

  • 对于任务导向性聊天机器人,我们可以使用任务是否完成来评估聊天机器人是否成功

  • Efficiency

  • Effectiveness

  • Usability

  • 针对非任务聊天机器人

  • 自动评估

  • 对话进行的长度/轮数

  • User sentiment analysis

  • Positive user responses / total user responses

  • 人类评估

  • Coherence

  • Appropriateness

  • Rating

  • 基于参考答案的评估指标,BLEU, ROUGE, METEOR (有可能不太准确)

也有文章指出,这种基于参考答案的评价指标不能够很好地反映聊天机器人的好坏

所以很多时候,我们对聊天机器人,尤其是闲聊机器人的评估依然依赖于人类评估。人们也开始尝试一些深度学习的模型来预测人类给模型的打分。

构建聊天机器人的三大模块

img

  • Coherence

  • User experience

  • Engagement Management

自然语言理解(Natural Language Understanding)

NLU的主要任务是从文本中提取信息,包括对话文本的意图,文本中关键的信息,例如命名实体,并且将这些信息转成比较标准化的表示方式以供后续聊天机器人模块使用。

面临的挑战

  • 去除口语化的表达

  • 话语重复

  • 讲话中自我修正

  • 口语化的表述和停顿

Frame-based SLU (Spoken Language Understanding)

Meaning Representation Language

把自然语言转变成一种固定的结构化数据表达形式

  • 有固定的语法结构

  • 计算机可执行的语言

Do you have any flights from Seattle to Boston on December 24th?

O O O O O O BDCity O BACity O BDDate IDDate

BiLSTM –> Classification (CRF)

img

Intent classification –> ShowFlight

POS

这种信息抽取的任务往往可以通过HMM, CRF等模型实现

other

B-Departure

I-Departure

B-Arrival

I-Arrival

B-Date

I-Date

conll-2003

意图识别 (Intent Classification)

Coarse-grained

  • 把用户的当前讲话的意图归类到我们提前指定的一些类别中去

  • 给定一句话 (utterance X),给定一系列 intent classes: C_1, …, C_M。预测当前 utterance 属于哪一个intent类别。

  • 同样的intent可能有很多不同的表达方式

  • 我想要预订后天北京到上海的机票

  • 能帮我订一张后天从首都机场到浦东机场的机票吗?

  • 后天从北京到虹桥的机票有吗?

img

命名实体识别

从一段文本中抽取命名实体

命名实体的类别

  • 机构、任务、地点、时间、日期、金钱、百分比

主要的方法

  • 基于HMM, CRF, RNN (LSTM) 等模型的 token 标注方法

关于 semantic parsing 和 slot filling

semantic parsing: 非结构化的语言转换成结构化的指令

https://nlpprogress.com/english/semantic_parsing.html

img

Find_Flight(Boston, New York)

semantic parsing

  • intent classification

  • arguments parsing

Open(“baidumap”, city=”Beijing”);

Open(“Google map”, city=”Beijing”);

Close(“Google map”, city=”Beijing”);

Install(“”)

BERT for Joint Intent Classification and Slot Filling

https://arxiv.org/pdf/1902.10909.pdf

Dialogue State Tracking

DSTC 比赛和数据集

http://camdial.org/~mh521/dstc/

对话管理

什么是对话管理?

  • 一般是聊天机器人的中心模块,控制整个聊天的过程。上面承接NLU,后面连着NLG,负责储存聊天的信息和状态(slot filling),根据已有的信息 (当前聊天记录和外部信息),决定下一步做什么。

  • 负责与一些外部的知识库做交互,例如Knowledge base,各种数据库

Dialogue Context Modeling

  • 聊天的过程往往是高度基于对话记录的,很多时候我们会用代词指代之前的名词或实体

  • 你想聊一些关于科技的还是民生的话题?

  • 聊第二个吧 –> 民生

  • 共指消解 (Coreference Resolution)

  • 有时候我们会省略一些显而易见的信息

  • 你打算什么时候吃饭?

  • 晚上7点(吃饭

  • 有时候我们会省略一些信息(当我们和一些聊天机器人聊天的时候)

  • 放点好听的音乐(放什么音乐?根据历史记录放?选用户最喜欢的音乐?)

  • 开灯 (开什么灯?床头灯?顶灯?客厅的灯?卧室的灯?)

聊天context的来源

  • 聊天历史,例如聊天历史的纯文本内容,聊天记录中提到过的命名实体主题等等

  • 任务记录

  • 对于一个任务型聊天机器人来说,聊天机器人往往会保存一些结构化的聊天记录,这些记录有时候被称为 form, frame, template, status graph。例如对于一个订机票聊天机器人来说,它需要收集用户的姓名,身份证号码,出发机场,到达机场,航班时间要求,价格要求,等等。然后才可以帮助用户做决策。

  • 我们需要知道哪些信息已经被收集到了,哪些信息还没有被收集到。然后根据需要收集的信息确定下一步机器人需要跟用户说什么。

Knowledge Base

根据聊天任务的不同,聊天机器人需要不同的Knowledge Base

  • 对于航班订票机器人来说,需要航班信息的数据库

  • 对于酒店预订机器人来说,需要酒店数据库

  • 对于闲聊机器人来说,各种闲聊中需要用到的信息,新闻、财经、电影娱乐等等

Dialogue Control (Dialogue Act)

根据Context, 聊天历史,knowledge base –> action 可能是rule based 决策,也可能是基于机器学习模型的决策。

  • 根据当前(从用户和其他数据来源)获取的信息,决定下一步需要采取怎样的行动

  • 可以做的决策有:

  • 从用户处收集更多的信息

  • 与用户确认之前的信息

  • 向用户输出一些信息

  • 一些设计要素

  • 由用户还是系统来主导对话

  • 是否要向用户解释自己的动作

对话的主导方

  • 用户主导

  • 用户来控制对话的进程

  • 系统不会自由发挥,而是跟随用户的思路

  • 在一些QA系统和基于搜索的系统中较为常见

  • 系统主导

  • 系统控制对话的进程

  • 系统决定了用户能说什么,不能说什么

  • 对于系统听不懂的话,系统可以忽略或者告诉用户自己无法解决

  • 在一些简单任务的机器人中很常见

  • 混合主导

  • 以上两种系统的混合版,可以是简单的组合,也可以设计地更复杂

Dialogue Control 的一些方法

  • 基于Finite state machine

  • 基于Frame

  • 基于统计学模型(机器学习)

  • AI planning

基于 Finite-State 的聊天控制

Finite State Automata 有限状态机

img

img

  • 系统完全控制了整场对话

  • 系统会一直向用户提问

  • 系统会忽略任何它不需要的信息

这种控制系统的好处是

  • 容易设计和实现,完全使用if-else语句

  • 功能非常确定,可控制

  • 其中State的transition可以基于非常复杂的条件和对话状态

坏处是

  • 没有什么灵活性,真的是一个机器人

  • 只能支持系统主导的对话

Frame-Based Dialogue Control

  • 预先指定了一张表格 (Frame),聊天机器人的目标就是把这张表格填满

img

我们可以预先指定一些问题,用来从用户处得到我们想要的信息

Slot Question
出发地 你从哪个城市或机场出发?
目的地 你要去哪个城市?
起飞日期 你的起飞日期是?
起飞时间 你想几点钟起飞?
航空公司 你有偏好的航空公司吗?
姓名 你的名字叫什么?
证件号码 你的身份证号码是多少? –> 护照

提问的先后不需要确定。用户也可以同时回答多个问题。

根据当前未知信息的组合,系统可以提出不同的问题

  • 未知信息(出发地、目的地):你的旅行路线是?

  • 未知信息(出发地):你的出发城市是哪里?

  • 未知信息(目的地):你的目的地是哪里?

  • 出发地+起飞日期:你打算哪天从哪个机场出发?

只要这张mapping的表格足够全面,我们就可以处理各种情况。

Frame-Based的方法比Finite-State要灵活一些,但本质上还是一个非常固定的方法,在有限的空间里完成一项特定的任务。

这两种方法的主要缺陷在于:

  • 需要花很多时间考虑各种情况,人为设计对话路线,有可能会出现一些情况没有被设计到。

intent (entities), state –> action , reward –> intent (entities), state –> action , reward –> intent (entities), state –> ?

optional below

基于统计模型的Dialogue Control

基于统计学和数据的聊天机器人模型

  • 一套固定的states S 聊天历史

  • 一套固定的actions A 下一句要讲的话

  • 一套系统performance的评价指标 reward 自己设计

  • 一个policy \pi,决定了在一个state下可以采取怎样的action (可能是一个神经网络)根据当前的聊天历史,决定下一句话讲什么

训练方法

  • 监督学习,需要很多的训练数据

  • 强化学习 (Reinforcement Learning),需要优化模型的最终回报 (return)

  • 除了上面的信息之外,还要加入一个回报函数

关于如何做强化学习?我们这里不再详细展开,感兴趣的同学可以阅读

这篇来自Jiwei Li的文章引用量很高

Deep Reinforcement Learning for Dialogue Generation

https://aclweb.org/anthology/D16-1127

在闲聊机器人中的Dialogue Control

  • 可能涉及到的话题空间较大,聊天的控制比较复杂

  • 没有明确的任务目标,很难定义reward函数,可能唯一的目标就是让聊天时间变长

常见的做法

  • 把聊天机器人分成几个不同的模块,每个模块可以负责一些聊天的子话题,或者由一些不同的模型实现

  • 有一个master模块负责分配聊天任务给不同的模块

阶梯化的模块

  • 聊天历史记录模块 (Dialogue State/Context Tracking)

  • Master Dialogue Manager

  • Miniskill Dialogue Manager

很多Miniskill Dialogue Manager是由finite-state-machine来实现的,可以通过引入一些non-deterministic finite automata来增加聊天的丰富度和多样性

action, slot values (key value pairs)

dialogue history

自然语言生成 Natural Language Generation (NLG)

Template based

display_flight_info

action: ask_departure_city

  • 你要从哪个机场离开?

  • 你从哪里起飞?

ask_time

使用模板来生成句子

  • [DEPARTURE-CITY]: 你打算几点钟离开 [DEPARTURE-CITY]?

  • [TOPIC]: 不如聊聊 [TOPIC]?

Retrieval Based

Response R**etri**eval

  • 根据当前的对话场景/历史决定提取怎样的回复

  • 基于retrieval而不是generation的方法

  • 可以使用机器学习的模型来训练抽取模型

  • 可以根据similarity mat**chin**g的方法:ELMo, average, cosine similarity. Google universal sentence encoder. 自己训练一个模型?

  • 可以利用一些别的基于搜索的方法

img

思考一下你会怎么构建这个模型?

可以用它来制作一个问答机器人,回答常见的问题。

基于深度学习的聊天机器人(偏向实验性质)

用Seq2Seq模型来做生成

img

  • 多样性很差

  • 很难控制

Hierarchical LSTM, Hierachical BERT?

在聊天机器人中使用生成模型有一个很大的问题,就是你无法完全掌控生成句子的各种属性,我们无法知道模型会生成什么样的句子。这也导致了基于神经网络的模型,例如Seq2Seq,在有任务的聊天机器人中并没有得到非常多的使用,而是更多地出现在一些娱乐性的项目之中。例如如果我们想要训练一只“小黄鸡”,那么你可以大胆地使用Seq2Seq等神经网络模型。可是如果你想要

Jiwei Li的一系列基于深度学习的Dialogue Generation

Jiwei 在 斯坦福的slides https://web.stanford.edu/class/cs224s/lectures/224s.17.lec12.pdf

Deep Reinforcement Learning for Dialogue Generation

使用深度增强学习来训练聊天机器人。

增强学习 Framework

action: 下一句要生成的话

state: 当前的聊天历史,在本文中使用最近的两句对话

policy: 一个基于LSTM的 Seq2Seq 模型

reward: 对当前生成对话的评价指标,本文中采用了三项指标。

  • Ease of answering: 这句对话是不是很容易回答,不要“把天聊死”。

  • Information Flow: 当前生成的对话应该和之前的聊天记录有所变化。

  • Semantic Coherence:上下文是否连贯。

Adversarial Learning for Neural Dialogue Generation

把GAN的想法用于聊天机器人的评估中。与上文相同,生成器是一个聊天机器人,可以生成对话。判别器是一个打分系统,可以对聊天机器人生成的对话进行打分。然后使用增强学习(REINFORCE, Policy Gradient算法)来训练生成器。

关于如何实现policy gradient

https://discuss.pytorch.org/t/whats-the-right-way-of-implementing-policy-gradient/4003/2

参考该项目 https://github.com/suragnair/seqGAN

Alexa Prize Challenge

2018年冠军 Gunrock: Building A Human-Like Social Bot By Leveraging Large Scale Real User Data

https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexaprize/assets/pdf/2018/Gunrock.pdf

img

img

2017年冠军 Sounding Board – University of Washington’s Alexa Prize Submission

https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/alexaprize/assets/pdf/2017/Soundingboard.pdf

img

img

参考课程资料

中文聊天机器人的资料

一些公司

数据集

  • movie dialogue dataset

  • ubuntu dialogue dataset

聊天机器人一

聊天机器人

流行平台框架与实战

这堂课主要是带大家一起看看国内外一些主流的聊天机器人搭建平台。

图灵机器人

link: http://www.tuling123.com/

title

传说中在中文语境下智能度最高的机器人大脑。

用法Demo

  1. 注册
  2. 创建自己的机器人
  3. 连接微信公众号或QQ
  4. 社区

wxBot

图灵机器人很棒,基本帮我们做到了傻瓜式接入主流中文社交软件。

但是,我们可以发现,这里依旧有限制,

官方提供的傻瓜式服务只能接入微信公众号或者QQ,

如果有更多的需求,那我们就得自己动手了。

比如,将个人微信变成聊天机器人

这里我们介绍一个很棒的GitHub项目:WxBot(credit: @liuwons)

link: https://github.com/liuwons/wxBot

这是一个是用Python包装Web微信协议实现的微信机器人框架。

换句话说,就是模拟了我们网页登录微信的状态,并通过网页微信协议传输对话。

好,接下来,我们来用wxbot实现一些有趣的功能:

群聊天机器人

(转载自:http://blog.csdn.net/tobacco5648/article/details/50802922

实现效果

title

有点像谷歌旗下的聊天工具 Allo

title

简单说就是,在你聊天的时候,可以随时把小机器人给at出来,并且拉出来跟你聊天。

同时,如果调用图灵机器人的服务的话,你可以让它做很多复杂的工作,

比如,找附近的商店啊,查火车时刻表啊,等等。

运行方法

  1. 下载wxBot, 安装python的依赖包。

    1
    2
    3
    4
    pip install requests
    pip install pyqrcode
    pip install pypng
    pip install Pillow
  2. 在图灵机器人官网注册账号,申请图灵API key

  3. 在bot.py文件所在目录下新建conf.ini文件,内容为(key字段内容为申请到的图灵key):

    1
    2
    [main]
    key=1d2678900f734aa0a23734ace8aec5b1
  4. 最后我们运行bot.py即可

运行之后,你的terminal会跳出一个二维码。

你按照登录网页版微信的方式,扫一下 登录一下。

你的微信就被”托管“了 >.<

In [ ]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
#!/usr/bin/env python
# coding: utf-8

from wxbot import *
import ConfigParser
import json

class TulingWXBot(WXBot):
# 拿到API Key
def __init__(self):
WXBot.__init__(self)

self.tuling_key = ""
self.robot_switch = True

try:
cf = ConfigParser.ConfigParser()
cf.read('conf.ini')
self.tuling_key = cf.get('main', 'key')
except Exception:
pass
print 'tuling_key:', self.tuling_key

# 从图灵API返回自动回复
def tuling_auto_reply(self, uid, msg):
if self.tuling_key:
# API url
url = "http://www.tuling123.com/openapi/api"
user_id = uid.replace('@', '')[:30]
# API request
body = {'key': self.tuling_key, 'info': msg.encode('utf8'), 'userid': user_id}
r = requests.post(url, data=body)
respond = json.loads(r.text)
result = ''
# 拿到回复,进行处理
# 按照API返回的code进行分类
# 是否是一个link
# 还是一句话
# 还是一组(list)回复
if respond['code'] == 100000:
result = respond['text'].replace('<br>', ' ')
result = result.replace(u'\xa0', u' ')
elif respond['code'] == 200000:
result = respond['url']
elif respond['code'] == 302000:
for k in respond['list']:
result = result + u"【" + k['source'] + u"】 " +\
k['article'] + "\t" + k['detailurl'] + "\n"
else:
result = respond['text'].replace('<br>', ' ')
result = result.replace(u'\xa0', u' ')

print ' ROBOT:', result
return result
# 加了个exception,如果没有图灵API Key的话,
# 那就无脑回知道了。
else:
return u"知道啦"

# 如果,用户不想机器人继续BB了,
# 或者,用户想言语调出机器人:
def auto_switch(self, msg):
msg_data = msg['content']['data']
stop_cmd = [u'退下', u'走开', u'关闭', u'关掉', u'休息', u'滚开']
start_cmd = [u'出来', u'启动', u'工作']
if self.robot_switch:
for i in stop_cmd:
if i == msg_data:
self.robot_switch = False
self.send_msg_by_uid(u'[Robot]' + u'机器人已关闭!', msg['to_user_id'])
else:
for i in start_cmd:
if i == msg_data:
self.robot_switch = True
self.send_msg_by_uid(u'[Robot]' + u'机器人已开启!', msg['to_user_id'])

# 从微信回复
def handle_msg_all(self, msg):
if not self.robot_switch and msg['msg_type_id'] != 1:
return
if msg['msg_type_id'] == 1 and msg['content']['type'] == 0: # reply to self
self.auto_switch(msg)
elif msg['msg_type_id'] == 4 and msg['content']['type'] == 0: # text message from contact
self.send_msg_by_uid(self.tuling_auto_reply(msg['user']['id'], msg['content']['data']), msg['user']['id'])
elif msg['msg_type_id'] == 3 and msg['content']['type'] == 0: # group text message
if 'detail' in msg['content']:
my_names = self.get_group_member_name(msg['user']['id'], self.my_account['UserName'])
if my_names is None:
my_names = {}
if 'NickName' in self.my_account and self.my_account['NickName']:
my_names['nickname2'] = self.my_account['NickName']
if 'RemarkName' in self.my_account and self.my_account['RemarkName']:
my_names['remark_name2'] = self.my_account['RemarkName']

is_at_me = False
for detail in msg['content']['detail']:
if detail['type'] == 'at':
for k in my_names:
if my_names[k] and my_names[k] == detail['value']:
is_at_me = True
break
if is_at_me:
src_name = msg['content']['user']['name']
reply = 'to ' + src_name + ': '
if msg['content']['type'] == 0: # text message
reply += self.tuling_auto_reply(msg['content']['user']['id'], msg['content']['desc'])
else:
reply += u"对不起,只认字,其他杂七杂八的我都不认识,,,Ծ‸Ծ,,"
self.send_msg_by_uid(reply, msg['user']['id'])


def main():
bot = TulingWXBot()
bot.DEBUG = True
bot.conf['qr'] = 'png'

bot.run()


if __name__ == '__main__':
main()

GitHub上还有不少相似的项目,大家都可以关注一下:

feit/Weixinbot Nodejs 封装网页版微信的接口,可编程控制微信消息

littlecodersh/ItChat 微信个人号接口、微信机器人及命令行微信,Command line talks through Wechat

Urinx/WeixinBot 网页版微信API,包含终端版微信及微信机器人

zixia/wechaty Wechaty is wechat for bot in Javascript(ES6). It’s a Personal Account Robot Framework/Library.

WxbotManage 基于Wxbot的微信多开管理和Webapi系统

自定义API接口

刚刚我们讲的部分,还都是调用图灵机器人的API。

我们来看看,如何使用自己的ChatBot模型。

我们这里用之前讲过的Chatterbot库做个例子。

思路还是一样。我们用WxBot来处理微信端的工作,

然后,我们架设一个API,来把chatterbot的回复给传到微信去。

这里我们用一个简单粗暴的API框架:Hug。

大家也可以用其他各种框架,比如Flask。

In [ ]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/env python
# coding: utf-8

# 导入chatterbot自带的语料库
from chatterbot import ChatBot
from chatterbot.trainers import ChatterBotCorpusTrainer
import hug

deepThought = ChatBot("deepThought")
deepThought.set_trainer(ChatterBotCorpusTrainer)
# 使用中文语料库训练它
deepThought.train("chatterbot.corpus.chinese") # 语料库

# API框架
@hug.get()
def get_response(user_input):
response = deepThought.get_response(user_input).text
return {"response":response}

hug -f bot_api.py

跑起来以后,你的terminal大概长这样:

title

于是你可以在浏览器中尝试:

title

好,

接下来的部分,依旧是感谢@liuwons的wxbot:

In [ ]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#!/usr/bin/env python
# coding: utf-8

from wxbot import WXBot
import requests

# base url
bot_api="http://127.0.0.1:8000/get_response"

# 处理回复
class MyWXBot(WXBot):
def handle_msg_all(self, msg):
if msg['msg_type_id'] == 4 and msg['content']['type'] == 0:
user_input = msg["content"]["data"]
payload={"user_input":user_input}
response = requests.get(bot_api,params=payload).json()["response"]
self.send_msg_by_uid(response, msg['user']['id'])

def main():
bot = MyWXBot()
bot.DEBUG = True
bot.conf['qr'] = 'png'
bot.run()

if __name__ == '__main__':
main()

聊起来大概是这个节奏。

title

好,如此,我们能做的显然不仅仅是用了个chatterbot

因为我们这样的一个结构,我们其实可以把任何聊天模型应用进来。

简单那来说,

Model <–API–> WxBot <–web–> WeChat

我们要做的就是封装一下我们的模型,让它看到一句话,给一个回复。

或者甚至是把VQA加到微信机器人中,发图片再发问题,求回复~

一些有趣的微信机器人的idea

  • 跨群转发。这是个非常实用的功能。对群来说,因为微信一个群最多500人, 跨群转发可以有效地把两个群拼到一起,实现更广泛的讨论。对个人来说,也可以用有选择的转发来把信息归档。比如看老板或者妹子在你加的几个群里每天都说了啥等等。

  • 聊天消息的主题归并,分析和搜索。微信聊天的基本单位是消息,但消息本身是非常碎片化的,很不适合搜索和分析。机器人可以把相关主题的消息归并起来,一方面可以大幅减小信息过载,一方面也可以从中得到更有价值的信息(类似视频分析里面把帧变成镜头)。这样分析以后可以做知识归档,用OneNote/印象笔记甚至公众号把讨论的成果沉淀下来。

  • 聊天脉络的梳理。群里的人一多,经常会出现几个话题并行出现的情况。这种情况对于理解和搜索都是非常不利的。机器人也需要把聊天的脉络进行梳理,在同一时间,把不同主题分别开。

  • 基本的统计数据。比如发言时间的分布,群的活跃度,成员的活跃度等等。做成漂亮的可视化,用户应该也会喜欢,给产品加分。

国际上比较主流的几大聊天机器人框架

  • wit.ai
  • api.ai
  • microsoft bot framework

虽然…

但是我们还是要稍微了解一下行业内的一些insights,

看看现在业内大火的各种炒聊天机器人概念的startups都是怎么玩的。

我们选点简单的开始玩起。

机器人端:api.ai,背后是谷歌

聊天端,我们选个Telegram,主打安全和快速的画风清奇的聊天软件~

Demo

  1. 注册api.ai
  2. 创建api.ai机器人
  3. 设置intents, entities等等
  4. 及时测试
  5. 走起telegram
  6. 跟机器人爸爸聊天, 设置
  7. avatar
  8. integrate
  9. 托管GitHub, Heroku
  10. 手机端试试玩儿
  11. avatar

直接调用api.ai进你自己的app/平台

其实这几个大平台都有自己的各个语言的官方支持库,让生活变得灰常简单:

In [ ]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
!/usr/bin/env python
-*- coding: utf-8 -*-

import os.path
import sys
import json
try:
import apiai
except ImportError:
sys.path.append(
os.path.join(os.path.dirname(os.path.realpath(__file__)), os.pardir)
)
import apiai

# api token
CLIENT_ACCESS_TOKEN = 'your client access token'

def main():
while(1):
ai = apiai.ApiAI(CLIENT_ACCESS_TOKEN)
request = ai.text_request()
request.lang = 'en' # 中文英文法语西语等等各种

# request.session_id = "<SESSION ID, UBIQUE FOR EACH USER>"
print("\n\nYour Input : ",end=" ")
request.query = input()

print("\n\nBot\'s response :",end=" ")
response = request.getresponse()
responsestr = response.read().decode('utf-8')
response_obj = json.loads(responsestr)

print(response_obj["result"]["fulfillment"]["speech"])

if __name__ == '__main__':
main()

跟之前的思路其实都差不多,

就是用框架的API调用来传送『对话』,解决问题

:P

就是这样!

In [ ]:

1
 

结构化预测

Beam search: greedy

P(y|x): x中文句子, y英文句子

|V|^n 50000^20

sampling:

dynamic programming: HMM

什么是结构化预测?

分类器

  • 将输入x匹配到输出y

  • 一种简单的分类器

  • 对任意一个输入x,计算每个可能的y的分数 score (x, y, \theta),其中\theta是模型参数

  • 选择分数最高的label y作为预测的类别

  • 一般来说,如果输出空间是指数(exponential)级别或者无限的,我们把这类问题成为结构化预测(structured prediction)

y = argmax_{candidate}(score(x, candidate, \theta))

结构化预测案例

  • POS Tagging

  • Unlabeled Segmentation

  • 莎拉波娃现在居住在美国东南部的佛罗里达。

  • 莎拉波娃 现在 居住 在 美国 东南部 的 佛罗里达 。

  • Labeled Segmentation (Named Entity Recognition 命名实体识别)

  • Some questioned if Tim Cook’s first product would be a breakaway hit for Apple.

img

img

  • Constituency Parsing

img

  • Coreference Resolution

img

  • 语言生成:有很多NLP任务涉及到生成一段话,一个短语,一篇文章等等

  • 问答系统

  • 文本翻译

  • 文本摘要

什么是结构化预测?

  • 比较官方的定义是,当parts functions无法被拆分成minimal parts的时候,这就是一个结构化预测的问题。但是我们这里不详细展开。

  • part是一个问题的一部分子问题

  • parts function:用来把输入/输出拆分成parts

  • parts可以重叠

  • minimal parts:该任务最小的parts

  • minimal parts是不重叠的

  • 结构化score/loss函数是没有办法拆分成minimal parts的。当我们使用一个结构化的score或者损失函数的时候,我们就在做structured predcition。

结构化预测解决的问题

序列标签问题

  • 输入长度为T

  • 输出长度也是T

  • 每个位置可能的候选是N个label中的一个

  • 输出空间为 N ^ T

序列标签的案例

  • 前向神经网络做POS tagging

  • 输入是一个单词和它相邻的单词

  • 输出是中心词的POS tag

  • 训练loss: 每个位置中心词的log loss,每个位置的loss相加

img

  • 这不是一个”结构化预测“问题

如果我们采用一个RNN模型来做词性标注,这就成为了一个结构化预测的问题。

img

如果我们使用HMM模型,这也是一个结构化预测问题。

img

  • 不要把语言当做“bag of words”,单词之间有“结构”。

训练完模型之后,如何很好地快速地做搜索?

Viterbi 算法

https://shimo.im/docs/TRvGRjwJP8TDrCjc

图解Viterbi维特比算法

https://zhuanlan.zhihu.com/p/63087935

Viterbi算法的时间复杂度比较高。(回忆一下Viterbi算法的时间复杂度是多少?)

所以人们发明了一些近似的算法,例如greedy decoding,以及Beam Seaech。

CRF模型

img

CRF的条件概率公式

img

分母是很大的

y 有五种不同的可能性

20 5^20

img

参考资料

http://www.robots.ox.ac.uk/~davidc/pubs/crfs_jan2015.pdf

http://www.cs.cmu.edu/~10715-f18/lectures/lecture2-crf.pdf

http://www.davidsbatista.net/blog/2017/11/13/Conditional_Random_Fields/

Michael Collins’ notes

Log Linear tagggers http://www.cs.columbia.edu/~mcollins/fall2014-loglineartaggers.pdf

CRF Model http://www.cs.columbia.edu/~mcollins/crf.pdf

Forward-Backward算法 http://www.cs.columbia.edu/~mcollins/fb.pdf

参考资料

SVM

机器学习中的SVM

支持向量机是一种经典的二分类模型,基本模型定义为特征空间中最大间隔的线性分类器,其学习的优化目标便是间隔最大化,因此支持向量机本身可以转化为一个凸二次规划求解的问题。

函数间隔与几何间隔

对于二分类学习,假设现在的数据是线性可分的,这时分类学习最基本的想法就是找到一个合适的超平面,该超平面能够将不同类别的样本分开,类似二维平面使用ax+by+c=0来表示,超平面实际上表示的就是高维的平面,如下图所示:

1.png

对数据点进行划分时,易知:当超平面距离与它最近的数据点的间隔越大,分类的鲁棒性越好,即当新的数据点加入时,超平面对这些点的适应性最强,出错的可能性最小。因此需要让所选择的超平面能够最大化这个间隔Gap(如下图所示), 常用的间隔定义有两种,一种称之为函数间隔,一种为几何间隔,下面将分别介绍这两种间隔,并对SVM为什么会选用几何间隔做了一些阐述。

2.png

函数间隔

在超平面w’x+b=0确定的情况下,|w’x+b|能够代表点x*距离超平面的远近,易知:当w’x+b>0时,表示x在超平面的一侧(正类,类标为1),而当w’x+b<0时,则表示x在超平面的另外一侧(负类,类别为-1),因此(w’x+b)y* 的正负性恰能表示数据点x是否被分类正确。于是便引出了*函数间隔**的定义(functional margin):

3.png

而超平面(w,b)关于所有样本点(Xi,Yi)的函数间隔最小值则为超平面在训练数据集T上的函数间隔:

4.png

可以看出:这样定义的函数间隔在处理SVM上会有问题,当超平面的两个参数w和b同比例改变时,函数间隔也会跟着改变,但是实际上超平面还是原来的超平面,并没有变化。例如:w1x1+w2x2+w3x3+b=0其实等价于2w1x1+2w2x2+2w3x3+2b=0,但计算的函数间隔却翻了一倍。从而引出了能真正度量点到超平面距离的概念–几何间隔(geometrical margin)。

几何间隔

几何间隔代表的则是数据点到超平面的真实距离,对于超平面w’x+b=0,w代表的是该超平面的法向量,设x为超平面外一点x在法向量w方向上的投影点,x与超平面的距离为r,则有x=x-r(w/||w||),又x在超平面上,即w’x+b=0,代入即可得:

5.png

为了得到r的绝对值,令r呈上其对应的类别y,即可得到几何间隔的定义:

6.png

从上述函数间隔与几何间隔的定义可以看出:实质上函数间隔就是|w’x+b|,而几何间隔就是点到超平面的距离。

最大间隔与支持向量

通过前面的分析可知:函数间隔不适合用来最大化间隔,因此这里我们要找的最大间隔指的是几何间隔,于是最大间隔分类器的目标函数定义为:

7.png

一般地,我们令r^为1(这样做的目的是为了方便推导和目标函数的优化),从而上述目标函数转化为:

8.png

对于y(w’x+b)=1的数据点,即下图中位于w’x+b=1或w’x+b=-1上的数据点,我们称之为支持向量(support vector),易知:对于所有的支持向量,它们恰好满足y(w’x+b)=1,而所有不是支持向量的点,有y(w’x+b)>1。

9.png

从原始优化问题到对偶问题

对于上述得到的目标函数,求1/||w||的最大值相当于求||w||^2的最小值,因此很容易将原来的目标函数转化为:

10.png

即变为了一个带约束的凸二次规划问题,按书上所说可以使用现成的优化计算包(QP优化包)求解,但由于SVM的特殊性,一般我们将原问题变换为它的对偶问题,接着再对其对偶问题进行求解。为什么通过对偶问题进行求解,有下面两个原因:

* 一是因为使用对偶问题更容易求解;
* 二是因为通过对偶问题求解出现了向量内积的形式,从而能更加自然地引出核函数。

对偶问题,顾名思义,可以理解成优化等价的问题,更一般地,是将一个原始目标函数的最小化转化为它的对偶函数最大化的问题。对于当前的优化问题,首先我们写出它的朗格朗日函数:

11.png

上式很容易验证:当其中有一个约束条件不满足时,L的最大值为 ∞(只需令其对应的α为 ∞即可);当所有约束条件都满足时,L的最大值为1/2||w||^2(此时令所有的α为0),因此实际上原问题等价于:

12.png

由于这个的求解问题不好做,因此一般我们将最小和最大的位置交换一下(需满足KKT条件) ,变成原问题的对偶问题:

13.png

这样就将原问题的求最小变成了对偶问题求最大(用对偶这个词还是很形象),接下来便可以先求L对w和b的极小,再求L对α的极大。

(1)首先求L对w和b的极小,分别求L关于w和b的偏导,可以得出:

14.png

将上述结果代入L得到:

15.png

(2)接着L关于α极大求解α(通过SMO算法求解,此处不做深入)。

16.png

(3)最后便可以根据求解出的α,计算出w和b,从而得到分类超平面函数。

17.png

在对新的点进行预测时,实际上就是将数据点x*代入分类函数f(x)=w’x+b中,若f(x)>0,则为正类,f(x)<0,则为负类,根据前面推导得出的w与b,分类函数如下所示,此时便出现了上面所提到的内积形式。

18.png

这里实际上只需计算新样本与支持向量的内积,因为对于非支持向量的数据点,其对应的拉格朗日乘子一定为0,根据最优化理论(K-T条件),对于不等式约束y(w’x+b)-1≥0,满足:

19.png

核函数

由于上述的超平面只能解决线性可分的问题,对于线性不可分的问题,例如:异或问题,我们需要使用核函数将其进行推广。一般地,解决线性不可分问题时,常常采用映射的方式,将低维原始空间映射到高维特征空间,使得数据集在高维空间中变得线性可分,从而再使用线性学习器分类。如果原始空间为有限维,即属性数有限,那么总是存在一个高维特征空间使得样本线性可分。若∅代表一个映射,则在特征空间中的划分函数变为:

20.png

按照同样的方法,先写出新目标函数的拉格朗日函数,接着写出其对偶问题,求L关于w和b的极大,最后运用SOM求解α。可以得出:

(1)原对偶问题变为:

21.png

(2)原分类函数变为:
22.png

求解的过程中,只涉及到了高维特征空间中的内积运算,由于特征空间的维数可能会非常大,例如:若原始空间为二维,映射后的特征空间为5维,若原始空间为三维,映射后的特征空间将是19维,之后甚至可能出现无穷维,根本无法进行内积运算了,此时便引出了核函数(Kernel)的概念。

23.png

因此,核函数可以直接计算隐式映射到高维特征空间后的向量内积,而不需要显式地写出映射后的结果,它虽然完成了将特征从低维到高维的转换,但最终却是在低维空间中完成向量内积计算,与高维特征空间中的计算等效(低维计算,高维表现),从而避免了直接在高维空间无法计算的问题。引入核函数后,原来的对偶问题与分类函数则变为:

(1)对偶问题:

24.png

(2)分类函数:

25.png

因此,在线性不可分问题中,核函数的选择成了支持向量机的最大变数,若选择了不合适的核函数,则意味着将样本映射到了一个不合适的特征空间,则极可能导致性能不佳。同时,核函数需要满足以下这个必要条件:

26.png

由于核函数的构造十分困难,通常我们都是从一些常用的核函数中选择,下面列出了几种常用的核函数:

27.png

软间隔支持向量机

前面的讨论中,我们主要解决了两个问题:当数据线性可分时,直接使用最大间隔的超平面划分;当数据线性不可分时,则通过核函数将数据映射到高维特征空间,使之线性可分。然而在现实问题中,对于某些情形还是很难处理,例如数据中有噪声的情形,噪声数据(outlier)本身就偏离了正常位置,但是在前面的SVM模型中,我们要求所有的样本数据都必须满足约束,如果不要这些噪声数据还好,当加入这些outlier后导致划分超平面被挤歪了,如下图所示,对支持向量机的泛化性能造成很大的影响。

28.png

为了解决这一问题,我们需要允许某一些数据点不满足约束,即可以在一定程度上偏移超平面,同时使得不满足约束的数据点尽可能少,这便引出了“软间隔”支持向量机的概念

* 允许某些数据点不满足约束y(w'x+b)≥1;
* 同时又使得不满足约束的样本尽可能少。

这样优化目标变为:

29.png

如同阶跃函数,0/1损失函数虽然表示效果最好,但是数学性质不佳。因此常用其它函数作为“替代损失函数”。

30.png

支持向量机中的损失函数为hinge损失,引入“松弛变量”,目标函数与约束条件可以写为:

31.png

其中C为一个参数,控制着目标函数与新引入正则项之间的权重,这样显然每个样本数据都有一个对应的松弛变量,用以表示该样本不满足约束的程度,将新的目标函数转化为拉格朗日函数得到:

32.png

按照与之前相同的方法,先让L求关于w,b以及松弛变量的极小,再使用SMO求出α,有:

33.png

将w代入L化简,便得到其对偶问题:

34.png

将“软间隔”下产生的对偶问题与原对偶问题对比可以发现:新的对偶问题只是约束条件中的α多出了一个上限C,其它的完全相同,因此在引入核函数处理线性不可分问题时,便能使用与“硬间隔”支持向量机完全相同的方法。

word-embedding

词向量

学习目标

  • 学习词向量的概念
  • 用Skip-thought模型训练词向量
  • 学习使用PyTorch dataset和dataloader
  • 学习定义PyTorch模型
  • 学习torch.nn中常见的Module
    • Embedding
  • 学习常见的PyTorch operations
    • bmm
    • logsigmoid
  • 保存和读取PyTorch模型

使用的训练数据可以从以下链接下载到。

链接:https://pan.baidu.com/s/1tFeK3mXuVXEy3EMarfeWvg 密码:v2z5

在这一份notebook中,我们会(尽可能)尝试复现论文Distributed Representations of Words and Phrases and their Compositionality中训练词向量的方法. 我们会实现Skip-gram模型,并且使用论文中noice contrastive sampling的目标函数。

这篇论文有很多模型实现的细节,这些细节对于词向量的好坏至关重要。我们虽然无法完全复现论文中的实验结果,主要是由于计算资源等各种细节原因,但是我们还是可以大致展示如何训练词向量。

以下是一些我们没有实现的细节

  • subsampling:参考论文section 2.3

In [1]:

1
from torch import nn

In [2]:

1
2
3
4
import torch
import torch.nn as nn #神经网络工具箱torch.nn
import torch.nn.functional as F #神经网络函数torch.nn.functional
import torch.utils.data as tud #Pytorch读取训练集需要用到torch.utils.data类

两个模块的区别:torch.nn 和 torch.functional 的区别

In [3]:

1
2
3
4
5
6
7
8
9
10
11
from torch.nn.parameter import Parameter  #参数更新和优化函数

from collections import Counter #Counter 计数器
import numpy as np
import random
import math

import pandas as pd
import scipy #SciPy是基于NumPy开发的高级模块,它提供了许多数学算法和函数的实现
import sklearn
from sklearn.metrics.pairwise import cosine_similarity #余弦相似度函数

开始看代码前,请确保对word2vec有了解。

CBOW模型理解

Skip-Gram模型理解

负例采样就是Skip-Gram模型的输出不是周围词的概率了,是正例和负例的概率

In [4]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
USE_CUDA = torch.cuda.is_available() #有GPU可以用

# 为了保证实验结果可以复现,我们经常会把各种random seed固定在某一个值
random.seed(53113)
np.random.seed(53113)
torch.manual_seed(53113)
if USE_CUDA:
torch.cuda.manual_seed(53113)

# 设定一些超参数
K = 10 # number of negative samples 负样本随机采样数量
C = 3 # nearby words threshold 指定周围三个单词进行预测
NUM_EPOCHS = 2 # The number of epochs of training 迭代轮数
MAX_VOCAB_SIZE = 30000 # the vocabulary size 词汇表多大
BATCH_SIZE = 128 # the batch size 每轮迭代1个batch的数量
LEARNING_RATE = 0.2 # the initial learning rate #学习率
EMBEDDING_SIZE = 100 #词向量维度


LOG_FILE = "word-embedding.log"

# tokenize函数,把一篇文本转化成一个个单词
def word_tokenize(text):
return text.split()
  • 从文本文件中读取所有的文字,通过这些文本创建一个vocabulary
  • 由于单词数量可能太大,我们只选取最常见的MAX_VOCAB_SIZE个单词
  • 我们添加一个UNK单词表示所有不常见的单词
  • 我们需要记录单词到index的mapping,以及index到单词的mapping,单词的count,单词的(normalized) frequency,以及单词总数。

In [5]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
with open("./text8/text8.train.txt", "r") as fin: #读入文件
text = fin.read() # 一次性读入文件所有内容

text = [w for w in word_tokenize(text.lower())]
#分词,在这里类似于text.split()
#print(len(text)) # 15313011,有辣么多单词

vocab = dict(Counter(text).most_common(MAX_VOCAB_SIZE-1))
#字典格式,把(MAX_VOCAB_SIZE-1)个最频繁出现的单词取出来,-1是留给不常见的单词
#print(len(vocab)) # 29999

vocab["<unk>"] = len(text) - np.sum(list(vocab.values()))
#unk表示不常见单词数=总单词数-常见单词数
# print(vocab["<unk>"]) # 617111
print(vocab["<unk>"])
idx_to_word = [word for word in vocab.keys()]
#取出字典的所有最常见30000单词

word_to_idx = {word:i for i, word in enumerate(idx_to_word)}
#取出所有单词的单词和对应的索引,索引值与单词出现次数相反,最常见单词索引为0。
1
617111

In [1]:

1
#print(vocab)

In [2]:

1
2
#print(list(word_to_idx.items())[29900:]) 
# 敲黑板:字典是怎么像列表那样切片的

In [9]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
word_counts = np.array([count for count in vocab.values()], dtype=np.float32)
#vocab所有单词的频数values

word_freqs = word_counts / np.sum(word_counts)
#所有单词的词频概率值
# print(np.sum(word_freqs))=1

word_freqs = word_freqs ** (3./4.)
#论文里乘以3/4次方
# print(np.sum(word_freqs)) = 7.7

word_freqs = word_freqs / np.sum(word_freqs) # 用来做 negative sampling
# 重新计算所有单词的频率,老师这里代码好像写错了
# print(np.sum(word_freqs)) = 1

VOCAB_SIZE = len(idx_to_word) #词汇表单词数30000=MAX_VOCAB_SIZE
VOCAB_SIZE

Out[9]:

1
30000

实现Dataloader

一个dataloader需要以下内容:

  • 把所有text编码成数字,然后用subsampling预处理这些文字。
  • 保存vocabulary,单词count,normalized word frequency
  • 每个iteration sample一个中心词
  • 根据当前的中心词返回context单词
  • 根据中心词sample一些negative单词
  • 返回单词的counts

这里有一个好的tutorial介绍如何使用PyTorch dataloader. 为了使用dataloader,我们需要定义以下两个function:

  • __len__ function需要返回整个数据集中有多少个item
  • __get__ 根据给定的index返回一个item

有了dataloader之后,我们可以轻松随机打乱整个数据集,拿到一个batch的数据等等。

torch.utils.data.DataLoader理解:https://blog.csdn.net/qq_36653505/article/details/83351808

In [10]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class WordEmbeddingDataset(tud.Dataset): #tud.Dataset父类
def __init__(self, text, word_to_idx, idx_to_word, word_freqs, word_counts):
''' text: a list of words, all text from the training dataset
word_to_idx: the dictionary from word to idx
idx_to_word: idx to word mapping
word_freq: the frequency of each word
word_counts: the word counts
'''
super(WordEmbeddingDataset, self).__init__() #初始化模型
self.text_encoded = [word_to_idx.get(t, VOCAB_SIZE-1) for t in text]
#字典 get() 函数返回指定键的值(第一个参数),如果值不在字典中返回默认值(第二个参数)。
#取出text里每个单词word_to_idx字典里对应的索引,不在字典里返回"<unk>"的索引=29999
# 这样text里的所有词都编码好了,从单词转化为了向量,
# 共有15313011个单词,词向量取值范围是0~29999,0是最常见单词向量。

self.text_encoded = torch.Tensor(self.text_encoded).long()
#变成tensor类型,这里变成longtensor,也可以torch.LongTensor(self.text_encoded)

self.word_to_idx = word_to_idx #保存数据
self.idx_to_word = idx_to_word #保存数据
self.word_freqs = torch.Tensor(word_freqs) #保存数据
self.word_counts = torch.Tensor(word_counts) #保存数据

def __len__(self): #数据集有多少个item
#魔法函数__len__
''' 返回整个数据集(所有单词)的长度
'''
return len(self.text_encoded) #所有单词的总数

def __getitem__(self, idx):
#魔法函数__getitem__,这个函数跟普通函数不一样
''' 这个function返回以下数据用于训练
- 中心词
- 这个单词附近的(positive)单词
- 随机采样的K个单词作为negative sample
'''
center_word = self.text_encoded[idx]
#中心词
#这里__getitem__函数是个迭代器,idx代表了所有的单词索引。

pos_indices = list(range(idx-C, idx)) + list(range(idx+1, idx+C+1))
#周围词的索引,比如idx=0时。pos_indices = [-3, -2, -1, 1, 2, 3]


pos_indices = [i%len(self.text_encoded) for i in pos_indices]
#超出词汇总数时,需要特别处理,取余数,比如pos_indices = [15313009,15313010,15313011,1,2,3]

pos_words = self.text_encoded[pos_indices]
#周围词,就是希望出现的正例单词

neg_words = torch.multinomial(self.word_freqs, K * pos_words.shape[0], True)
#负例采样单词,torch.multinomial作用是按照self.word_freqs的概率做K * pos_words.shape[0]次取值,
#输出的是self.word_freqs对应的下标。取样方式采用有放回的采样,并且self.word_freqs数值越大,取样概率越大。
#每个正确的单词采样K个,pos_words.shape[0]是正确单词数量=6

return center_word, pos_words, neg_words

创建dataset和dataloader

In [11]:

1
2
dataset = WordEmbeddingDataset(text, word_to_idx, idx_to_word, word_freqs, word_counts)
# list(dataset) 可以把尝试打印下center_word, pos_words, neg_words看看

torch.utils.data.DataLoader理解:https://blog.csdn.net/qq_36653505/article/details/83351808

In [12]:

1
dataloader = tud.DataLoader(dataset, batch_size=BATCH_SIZE, shuffle=True, num_workers=4)

In [13]:

1
2
3
print(next(iter(dataloader))[0].shape) # 一个batch中间词维度
print(next(iter(dataloader))[1].shape) # 一个batch周围词维度
print(next(iter(dataloader))[2].shape) # 一个batch负样本维度
1
2
3
torch.Size([128])
torch.Size([128, 6])
torch.Size([128, 60])

定义PyTorch模型

In [14]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
class EmbeddingModel(nn.Module):
def __init__(self, vocab_size, embed_size):
''' 初始化输出和输出embedding
'''
super(EmbeddingModel, self).__init__()
self.vocab_size = vocab_size #30000
self.embed_size = embed_size #100

initrange = 0.5 / self.embed_size
self.out_embed = nn.Embedding(self.vocab_size, self.embed_size, sparse=False)
#模型输出nn.Embedding(30000, 100)
self.out_embed.weight.data.uniform_(-initrange, initrange)
#权重初始化的一种方法


self.in_embed = nn.Embedding(self.vocab_size, self.embed_size, sparse=False)
#模型输入nn.Embedding(30000, 100)
self.in_embed.weight.data.uniform_(-initrange, initrange)
#权重初始化的一种方法


def forward(self, input_labels, pos_labels, neg_labels):
'''
input_labels: 中心词, [batch_size]
pos_labels: 中心词周围 context window 出现过的单词 [batch_size * (window_size * 2)]
neg_labelss: 中心词周围没有出现过的单词,从 negative sampling 得到 [batch_size, (window_size * 2 * K)]

return: loss, [batch_size]
'''

batch_size = input_labels.size(0) #input_labels是输入的标签,tud.DataLoader()返回的。已经被分成batch了。

input_embedding = self.in_embed(input_labels)
# B * embed_size
#这里估计进行了运算:(128,30000)*(30000,100)= 128(Batch) * 100 (embed_size)

pos_embedding = self.out_embed(pos_labels) # B * (2*C=6) * embed_size
# 这里估计进行了运算:(128,6,30000)*(128,30000,100)= 128(Batch) * 6 * 100 (embed_size)
#同上,增加了维度(2*C),表示一个batch有128组周围词单词,一组周围词有(2*C)个单词,每个单词有embed_size个维度。

neg_embedding = self.out_embed(neg_labels) # B * (2*C * K) * embed_size
#同上,增加了维度(2*C*K)


#torch.bmm()为batch间的矩阵相乘(b,n.m)*(b,m,p)=(b,n,p)
log_pos = torch.bmm(pos_embedding, input_embedding.unsqueeze(2)).squeeze() # B * (2*C)
# log_pos = (128,6,100)*(128,100,1) = (128,6,1) = (128,6)
# 这里如果没有负采样,只有周围单词来训练的话,每个周围单词30000个one-hot向量的维度
# 而负采样大大降低了维度,每个周围单词仅仅只有一个维度。每个样本输出共有2*C个维度
log_neg = torch.bmm(neg_embedding, -input_embedding.unsqueeze(2)).squeeze() # B * (2*C*K)
# log_neg = (128,6*K,100)*(128,100,1) = (128,6*K,1) = (128,6*K),注意这里有个负号,区别与正样本
# unsqueeze(2)指定位置升维,.squeeze()压缩维度。
# 而负采样降低了维度,每个负例单词仅仅只有一个维度,每个样本输出共有2*C*K个维度

#下面loss计算就是论文里的公式
log_pos = F.logsigmoid(log_pos).sum(1)
log_neg = F.logsigmoid(log_neg).sum(1) # batch_size
loss = log_pos + log_neg # 正样本损失和负样本损失和尽量最大

return -loss # 最大转化成最小

#取出self.in_embed数据参数,维度:(30000,100),就是我们要训练的词向量
# 这里本来模型训练有两个矩阵的,self.in_embed和self.out_embed两个
# 只是作者认为输入矩阵比较好,就舍弃了输出矩阵。
def input_embeddings(self):
return self.in_embed.weight.data.cpu().numpy()

定义一个模型以及把模型移动到GPU

In [15]:

1
2
3
4
5
model = EmbeddingModel(VOCAB_SIZE, EMBEDDING_SIZE)
#得到model,有参数,有loss,可以优化了

if USE_CUDA:
model = model.cuda()

In [28]:

1
data.iloc[:, 0:2].index

Out[28]:

1
RangeIndex(start=0, stop=353, step=1)

In [ ]:

1
2
3
4
5
6
7
8
data = pd.read_csv("wordsim353.csv", sep=",")
# else:
# data = pd.read_csv("simlex-999.txt", sep="\t")
print(data.head())
human_similarity = []
model_similarity = []
for i in data.iloc[:, 0:2].index:
print(i)

下面是评估模型的代码,以及训练模型的代码

In [16]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def evaluate(filename, embedding_weights): 
# 传入的有三个文件课选择,两个txt,一个csv,可以先自己打开看看
# embedding_weights是训练之后的embedding向量。
if filename.endswith(".csv"):
data = pd.read_csv(filename, sep=",") # csv文件打开
else:
data = pd.read_csv(filename, sep="\t") # txt文件打开,以\t制表符分割
human_similarity = []
model_similarity = []
for i in data.iloc[:, 0:2].index: # 这里只是取出行索引,用data.index也可以
word1, word2 = data.iloc[i, 0], data.iloc[i, 1] # 依次取出每行的2个单词
if word1 not in word_to_idx or word2 not in word_to_idx:
# 如果取出的单词不在我们建的30000万个词汇表,就舍弃,评估不了
continue
else:
word1_idx, word2_idx = word_to_idx[word1], word_to_idx[word2]
# 否则,分别取出这两个单词对应的向量,
word1_embed, word2_embed = embedding_weights[[word1_idx]], embedding_weights[[word2_idx]]
# 在分别取出这两个单词对应的embedding向量,具体为啥是这种取出方式[[word1_idx]],可以自行研究
model_similarity.append(float(sklearn.metrics.pairwise.cosine_similarity(word1_embed, word2_embed)))
# 用余弦相似度计算这两个100维向量的相似度。这个是模型算出来的相似度
human_similarity.append(float(data.iloc[i, 2]))
# 这个是人类统计得到的相似度

return scipy.stats.spearmanr(human_similarity, model_similarity)# , model_similarity
# 因为相似度是浮点数,不是0 1 这些固定标签值,所以不能用准确度评估指标
# scipy.stats.spearmanr网址:https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.spearmanr.html
# scipy.stats.spearmanr评估两个分布的相似度,有两个返回值correlation, pvalue
# correlation是评估相关性的指标(-1,1),越接近1越相关,pvalue值大家可以自己搜索理解

训练模型:

  • 模型一般需要训练若干个epoch
  • 每个epoch我们都把所有的数据分成若干个batch
  • 把每个batch的输入和输出都包装成cuda tensor
  • forward pass,通过输入的句子预测每个单词的下一个单词
  • 用模型的预测和正确的下一个单词计算cross entropy loss
  • 清空模型当前gradient
  • backward pass
  • 更新模型参数
  • 每隔一定的iteration输出模型在当前iteration的loss,以及在验证数据集上做模型的评估

In [17]:

1
2
3
4
for i, (input_labels, pos_labels, neg_labels) in enumerate(dataloader):
print(input_labels.shape, pos_labels.shape, neg_labels.shape)
if i>5:
break
1
2
3
4
5
6
7
torch.Size([128]) torch.Size([128, 6]) torch.Size([128, 60])
torch.Size([128]) torch.Size([128, 6]) torch.Size([128, 60])
torch.Size([128]) torch.Size([128, 6]) torch.Size([128, 60])
torch.Size([128]) torch.Size([128, 6]) torch.Size([128, 60])
torch.Size([128]) torch.Size([128, 6]) torch.Size([128, 60])
torch.Size([128]) torch.Size([128, 6]) torch.Size([128, 60])
torch.Size([128]) torch.Size([128, 6]) torch.Size([128, 60])

In [ ]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
optimizer = torch.optim.SGD(model.parameters(), lr=LEARNING_RATE)
#随机梯度下降

for e in range(NUM_EPOCHS): #开始迭代
for i, (input_labels, pos_labels, neg_labels) in enumerate(dataloader):
#print(input_labels, pos_labels, neg_labels)

# TODO
input_labels = input_labels.long() #longtensor
pos_labels = pos_labels.long()
neg_labels = neg_labels.long()
if USE_CUDA: # 变成cuda类型
input_labels = input_labels.cuda()
pos_labels = pos_labels.cuda()
neg_labels = neg_labels.cuda()

#下面第一节课都讲过的
optimizer.zero_grad() #梯度归零
loss = model(input_labels, pos_labels, neg_labels).mean()
# model返回的是一个batch所有样本的损失,需要求个平均

loss.backward()
optimizer.step()

#打印结果。
if i % 100 == 0:
with open(LOG_FILE, "a") as fout: # 写进日志文件,LOG_FILE前面定义了
fout.write("epoch: {}, iter: {}, loss: {}\n".format(e, i, loss.item()))
print("epoch: {}, iter: {}, loss: {}".format(e, i, loss.item()))
# 训练过程,我没跑,本地肯定跑不动的


if i % 2000 == 0: # 每过2000个batch就评估一次效果
embedding_weights = model.input_embeddings()
# 取出(30000,100)训练的词向量
sim_simlex = evaluate("simlex-999.txt", embedding_weights)
sim_men = evaluate("men.txt", embedding_weights)
sim_353 = evaluate("wordsim353.csv", embedding_weights)
with open(LOG_FILE, "a") as fout:
print("epoch: {}, iteration: {}, simlex-999: {}, men: {}, sim353: {}, nearest to monster: {}\n".format(
e, i, sim_simlex, sim_men, sim_353, find_nearest("monster")))
fout.write("epoch: {}, iteration: {}, simlex-999: {}, men: {}, sim353: {}, nearest to monster: {}\n".format(
e, i, sim_simlex, sim_men, sim_353, find_nearest("monster")))

embedding_weights = model.input_embeddings() # 调用最终训练好的embeding词向量
np.save("embedding-{}".format(EMBEDDING_SIZE), embedding_weights) # 保存参数
torch.save(model.state_dict(), "embedding-{}.th".format(EMBEDDING_SIZE)) # 保存参数

In [11]:

1
model.load_state_dict(torch.load("embedding-{}.th".format(EMBEDDING_SIZE))) # 加载模型

在 MEN 和 Simplex-999 数据集上做评估

In [12]:

1
2
3
4
5
# 代码同上
embedding_weights = model.input_embeddings()
print("simlex-999", evaluate("simlex-999.txt", embedding_weights))
print("men", evaluate("men.txt", embedding_weights))
print("wordsim353", evaluate("wordsim353.csv", embedding_weights))
1
2
3
simlex-999 SpearmanrResult(correlation=0.17251697429101504, pvalue=7.863946056740345e-08)
men SpearmanrResult(correlation=0.1778096817088841, pvalue=7.565661657312768e-20)
wordsim353 SpearmanrResult(correlation=0.27153702278146635, pvalue=8.842165885381714e-07)

寻找nearest neighbors

In [13]:

1
2
3
4
5
6
7
8
9
def find_nearest(word):
index = word_to_idx[word]
embedding = embedding_weights[index] # 取出这个单词的embedding向量
cos_dis = np.array([scipy.spatial.distance.cosine(e, embedding) for e in embedding_weights])
# 计算所有30000个embedding向量与传入单词embedding向量的相似度距离
return [idx_to_word[i] for i in cos_dis.argsort()[:10]] # 返回前10个最相似的

for word in ["good", "fresh", "monster", "green", "like", "america", "chicago", "work", "computer", "language"]:
print(word, find_nearest(word))
1
2
3
4
5
6
7
8
9
10
good ['good', 'bad', 'perfect', 'hard', 'questions', 'alone', 'money', 'false', 'truth', 'experience']
fresh ['fresh', 'grain', 'waste', 'cooling', 'lighter', 'dense', 'mild', 'sized', 'warm', 'steel']
monster ['monster', 'giant', 'robot', 'hammer', 'clown', 'bull', 'demon', 'triangle', 'storyline', 'slogan']
green ['green', 'blue', 'yellow', 'white', 'cross', 'orange', 'black', 'red', 'mountain', 'gold']
like ['like', 'unlike', 'etc', 'whereas', 'animals', 'soft', 'amongst', 'similarly', 'bear', 'drink']
america ['america', 'africa', 'korea', 'india', 'australia', 'turkey', 'pakistan', 'mexico', 'argentina', 'carolina']
chicago ['chicago', 'boston', 'illinois', 'texas', 'london', 'indiana', 'massachusetts', 'florida', 'berkeley', 'michigan']
work ['work', 'writing', 'job', 'marx', 'solo', 'label', 'recording', 'nietzsche', 'appearance', 'stage']
computer ['computer', 'digital', 'electronic', 'audio', 'video', 'graphics', 'hardware', 'software', 'computers', 'program']
language ['language', 'languages', 'alphabet', 'arabic', 'grammar', 'pronunciation', 'dialect', 'programming', 'chinese', 'spelling']

单词之间的关系

In [14]:

1
2
3
4
5
6
7
man_idx = word_to_idx["man"] 
king_idx = word_to_idx["king"]
woman_idx = word_to_idx["woman"]
embedding = embedding_weights[woman_idx] - embedding_weights[man_idx] + embedding_weights[king_idx]
cos_dis = np.array([scipy.spatial.distance.cosine(e, embedding) for e in embedding_weights])
for i in cos_dis.argsort()[:20]:
print(idx_to_word[i])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
king
henry
charles
pope
queen
iii
prince
elizabeth
alexander
constantine
edward
son
iv
louis
emperor
mary
james
joseph
frederick
francis

酒店评价情感分类与CNN模型

酒店评价情感分类与CNN模型

参考了https://github.com/bentrevett/pytorch-sentiment-analysis

我们会用PyTorch模型来做情感分析(检测一段文字的情感是正面的还是负面的)。我们会使用ChnSentiCorp_htl数据集,即酒店评论数据集。

数据下载链接:https://raw.githubusercontent.com/SophonPlus/ChineseNlpCorpus/master/datasets/ChnSentiCorp_htl_all/ChnSentiCorp_htl_all.csv

模型从简单到复杂,我们会依次构建:

  • Word Averaging模型
  • RNN/LSTM模型
  • CNN模型

准备数据

  • 首先让我们加载数据,来看看这一批酒店评价数据长得怎样

In [1]:

1
2
3
4
5
6
7
8
import pandas as pd
import numpy as np
path = "ChnSentiCorp_htl_all.csv"
pd_all = pd.read_csv(path)

print('评论数目(总体):%d' % pd_all.shape[0])
print('评论数目(正向):%d' % pd_all[pd_all.label==1].shape[0])
print('评论数目(负向):%d' % pd_all[pd_all.label==0].shape[0])
1
2
3
评论数目(总体):7766
评论数目(正向):5322
评论数目(负向):2444

In [2]:

1
pd_all.sample(5)

Out[2]:

label review
914 1 地点看上去不错,在北京西客站对面,但出行十分不便,周边没有地铁,门口出租车倒是挺多,但就是不…
7655 0 酒店位置较偏僻,环境清净,交通也方便,但酒店及周边就餐选择不多;浴场海水中有水草,水亦太浅,…
3424 1 酒店给人感觉很温欣,服务员也挺有礼貌,房间内的舒适度也非常不错,离开李公递也很近,下次来苏州…
4854 1 离故宫不太远,走路大概10分钟不到点,环境还好,有一点非常不好的是窗帘就只有一层,早上很早就…
5852 0 宾馆背面就是省道,交通是方便的,停车场很大也很方便,但晚上尤其半夜路过的汽车声音很响,拖拉机…

In [3]:

1
2
3
4
5
import pkuseg

seg = pkuseg.pkuseg() # 以默认配置加载模型
text = seg.cut('我爱北京天安门') # 进行分词
print(text)
1
['我', '爱', '北京', '天安门']

下面我们先手工把数据分成train, dev, test三个部分

In [4]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pd_all_shuf = pd_all.sample(frac=1)

# 总共有多少ins
total_num_ins = pd_all_shuf.shape[0]
pd_train = pd_all_shuf.iloc[:int(total_num_ins*0.8)]
pd_dev = pd_all_shuf.iloc[int(total_num_ins*0.8):int(total_num_ins*0.9)]
pd_test = pd_all_shuf.iloc[int(total_num_ins*0.9):]

# text, label
train_text = [seg.cut(str(text)) for text in pd_train.review.tolist()]
dev_text = [seg.cut(str(text)) for text in pd_dev.review.tolist()]
test_text = [seg.cut(str(text)) for text in pd_test.review.tolist()]
train_label = pd_train.label.tolist()
dev_label = pd_dev.label.tolist()
test_label = pd_test.label.tolist()

In [6]:

1
train_label[0]

Out[6]:

1
0

我们从训练数据构造出一个由单词到index的单词表

In [7]:

1
2
3
4
5
6
7
8
9
10
11
12
from collections import Counter
def build_vocab(sents, max_words=50000):
word_counts = Counter()
for sent in sents:
for word in sent:
word_counts[word] += 1
itos = [w for w, c in word_counts.most_common(max_words)]
itos = ["UNK", "PAD"] + itos
stoi = {w:i for i, w in enumerate(itos)}
return itos, stoi

itos, stoi = build_vocab(train_text)

查看一下比较高频的单词

In [8]:

1
itos[:10]

Out[8]:

1
['UNK', 'PAD', ',', '的', '。', '了', ',', '酒店', '是', '很']

In [10]:

1
stoi["酒店"]

Out[10]:

1
7

我们把文本中的单词都转换成index

In [12]:

1
2
3
train_idx = [[stoi.get(word, stoi.get("UNK")) for word in text] for text in train_text ]
dev_idx = [[stoi.get(word, stoi.get("UNK")) for word in text] for text in dev_text ]
test_idx = [[stoi.get(word, stoi.get("UNK")) for word in text] for text in test_text ]

把数据和label都转成batch

In [15]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def get_minibatches(text_idx, labels, batch_size=64, sort=True):
if sort:
text_idx_and_labels = sorted(list(zip(text_idx, labels)), key=lambda x: len(x[0]))

text_idx_batches = []
label_batches = []
for i in range(0, len(text_idx), batch_size):
text_batch = [t for t, l in text_idx_and_labels[i:i+batch_size]]
label_batch = [l for t, l in text_idx_and_labels[i:i+batch_size]]
max_len = max([len(t) for t in text_batch])
text_batch_np = np.ones((len(text_batch), max_len), dtype=np.int) # batch_size * max_seq_ength
for i, t in enumerate(text_batch):
text_batch_np[i, :len(t)] = t
text_idx_batches.append(text_batch_np)
label_batches.append(np.array(label_batch))

return text_idx_batches, label_batches

train_batches, train_label_batches = get_minibatches(train_idx, train_label)
dev_batches, dev_label_batches = get_minibatches(dev_idx, dev_label)
test_batches, test_label_batches = get_minibatches(test_idx, test_label)

In [17]:

1
train_batches[20]

Out[17]:

1
2
3
4
5
6
7
array([[  80,  177,  149, ...,  191,    3,    1],
[ 49, 18, 20, ..., 53, 4, 1],
[ 7, 18, 17, ..., 702, 4, 1],
...,
[1107, 2067, 10, ..., 748, 172, 442],
[ 241, 9, 19, ..., 17, 44, 30],
[3058, 20, 6, ..., 9, 19, 98]])

In [18]:

1
train_label_batches[20]

Out[18]:

1
2
3
array([1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1,
1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0,
1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1])
  • 和之前一样,我们会设定random seeds使实验可以复现。

In [19]:

1
2
3
4
5
6
7
8
9
10
11
import torch
from torchtext import data
import random

SEED = 1234

torch.manual_seed(SEED)
torch.cuda.manual_seed(SEED)
torch.backends.cudnn.deterministic = True

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

Word Averaging模型

  • 我们首先介绍一个简单的Word Averaging模型。这个模型非常简单,我们把每个单词都通过Embedding层投射成word embedding vector,然后把一句话中的所有word vector做个平均,就是整个句子的vector表示了。接下来把这个sentence vector传入一个Linear层,做分类即可。

In [32]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import torch
import torch.nn as nn
import torch.nn.functional as F

class WordAVGModel(nn.Module):
def __init__(self, vocab_size, embedding_size, output_size, pad_idx, dropout_p=0.2):
super(WordAVGModel, self).__init__()
self.embed = nn.Embedding(vocab_size, embedding_size, padding_idx=pad_idx)
self.linear = nn.Linear(embedding_size, output_size)
self.dropout = nn.Dropout(dropout_p) # 这个参数经常拿来调节

def forward(self, text, mask):
# text: batch_size * max_seq_len
# mask: batch_size * max_seq_len
embedded = self.embed(text) # [batch_size, max_seq_len, embedding_size]
embedded = self.dropout(embedded)
# dropout
mask = (1. - mask.float()).unsqueeze(2) # [batch_size, seq_len, 1], 1 represents word, 0 represents padding
embedded = embedded * mask # [batch_size, seq_len, embedding_size]
# 求平均
sent_embed = embedded.sum(1) / (mask.sum(1) + 1e-9) # 防止mask.sum为0,那么不能除以零。
# dropout
return self.linear(sent_embed)

In [75]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import torch
import torch.nn as nn
import torch.nn.functional as F

class WordMaxModel(nn.Module):
def __init__(self, vocab_size, embedding_size, output_size, pad_idx, dropout_p=0.2):
super(WordMaxModel, self).__init__()
self.embed = nn.Embedding(vocab_size, embedding_size, padding_idx=pad_idx)
self.linear = nn.Linear(embedding_size, output_size)
self.dropout = nn.Dropout(dropout_p) # 这个参数经常拿来调节

def forward(self, text, mask):
# text: batch_size * max_seq_len
# mask: batch_size * max_seq_len
embedded = self.embed(text) # [batch_size, max_seq_len, embedding_size]
embedded = self.dropout(embedded)
embedded.masked_fill(mask.unsqueeze(2), -999999)
# dropout
sent_embed = torch.max(embedded, 1)[0]
# dropout
return self.linear(sent_embed)

In [76]:

1
2
3
4
5
6
7
8
9
VOCAB_SIZE = len(itos)
EMBEDDING_SIZE = 100
OUTPUT_SIZE = 1
PAD_IDX = stoi["PAD"]

model = WordMaxModel(vocab_size=VOCAB_SIZE,
embedding_size=EMBEDDING_SIZE,
output_size=OUTPUT_SIZE,
pad_idx=PAD_IDX)

In [77]:

1
VOCAB_SIZE

Out[77]:

1
24001

In [78]:

1
2
3
4
5
# model
def count_parameters(model):
return sum(p.numel() for p in model.parameters() if p.requires_grad)

count_parameters(model)

Out[78]:

1
2400201

In [79]:

1
UNK_IDX = stoi["UNK"]

训练模型

In [80]:

1
2
3
4
5
optimizer = torch.optim.Adam(model.parameters())
crit = nn.BCEWithLogitsLoss()

model = model.to(device)
# crit = crit.to(device)

计算预测的准确率

In [81]:

1
2
3
4
5
def binary_accuracy(preds, y):
rounded_preds = torch.round(torch.sigmoid(preds))
correct = (rounded_preds == y).float()
acc = correct.sum() / len(correct)
return acc

In [82]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def train(model, text_idxs, labels, optimizer, crit):
epoch_loss, epoch_acc = 0., 0.
model.train()
total_len = 0.
for text, label in zip(text_idxs, labels):
text = torch.from_numpy(text).to(device)
label = torch.from_numpy(label).to(device)
mask = text == PAD_IDX
preds = model(text, mask).squeeze() # [batch_size, sent_length]
loss = crit(preds, label.float())
acc = binary_accuracy(preds, label)

# sgd
optimizer.zero_grad()
loss.backward()
optimizer.step()

# print("batch loss: {}".format(loss.item()))

epoch_loss += loss.item() * len(label)
epoch_acc += acc.item() * len(label)
total_len += len(label)

return epoch_loss / total_len, epoch_acc / total_len

In [83]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def evaluate(model, text_idxs, labels, crit):
epoch_loss, epoch_acc = 0., 0.
model.eval()
total_len = 0.
for text, label in zip(text_idxs, labels):
text = torch.from_numpy(text).to(device)
label = torch.from_numpy(label).to(device)
mask = text == PAD_IDX
with torch.no_grad():
preds = model(text, mask).squeeze()
loss = crit(preds, label.float())
acc = binary_accuracy(preds, label)

epoch_loss += loss.item() * len(label)
epoch_acc += acc.item() * len(label)
total_len += len(label)
model.train()

return epoch_loss / total_len, epoch_acc / total_len

In [84]:

1
2
3
4
5
6
7
8
9
10
11
12
N_EPOCHS = 10
best_valid_acc = 0.
for epoch in range(N_EPOCHS):
train_loss, train_acc = train(model, train_batches, train_label_batches, optimizer, crit)
valid_loss, valid_acc = evaluate(model, dev_batches, dev_label_batches, crit)

if valid_acc > best_valid_acc:
best_valid_acc = valid_acc
torch.save(model.state_dict(), "wordavg-model.pth")

print("Epoch", epoch, "Train Loss", train_loss, "Train Acc", train_acc)
print("Epoch", epoch, "Valid Loss", valid_loss, "Valid Acc", valid_acc)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Epoch 0 Train Loss 0.6241851981347865 Train Acc 0.6785254346426272
Epoch 0 Valid Loss 0.7439712684126895 Valid Acc 0.396396396434752
Epoch 1 Train Loss 0.6111872896254639 Train Acc 0.6775595621377978
Epoch 1 Valid Loss 0.7044879783381213 Valid Acc 0.4761904762288318
Epoch 2 Train Loss 0.5826128212314072 Train Acc 0.7041210560206053
Epoch 2 Valid Loss 0.667004818775172 Valid Acc 0.5791505791889348
Epoch 3 Train Loss 0.5516750626769744 Train Acc 0.7293947198969736
Epoch 3 Valid Loss 0.6347547087583456 Valid Acc 0.6473616476684924
Epoch 4 Train Loss 0.5236469646921791 Train Acc 0.7512878300064392
Epoch 4 Valid Loss 0.5953507212444928 Valid Acc 0.7348777351845799
Epoch 5 Train Loss 0.4969042095152394 Train Acc 0.7707662588538313
Epoch 5 Valid Loss 0.5561252224092471 Valid Acc 0.7786357789426237
Epoch 6 Train Loss 0.46501466702892946 Train Acc 0.797005795235029
Epoch 6 Valid Loss 0.5206214915217887 Valid Acc 0.8018018021086468
Epoch 7 Train Loss 0.43595607032794303 Train Acc 0.8163232453316163
Epoch 7 Valid Loss 0.4846100037776058 Valid Acc 0.8159588161889497
Epoch 8 Train Loss 0.40671270164611334 Train Acc 0.8386992916934964
Epoch 8 Valid Loss 0.45964578196809097 Valid Acc 0.8211068213369549
Epoch 9 Train Loss 0.38044816804408105 Train Acc 0.8539922730199614
Epoch 9 Valid Loss 0.4279780917953187 Valid Acc 0.8416988419289755

In [85]:

1
model.load_state_dict(torch.load("wordavg-model.pth"))

Out[85]:

1
<All keys matched successfully>

In [86]:

1
2
3
4
5
6
7
8
9
10
def predict_sentiment(model, sentence):
model.eval()
indexed = [stoi.get(t, PAD_IDX) for t in seg.cut(sentence)]
tensor = torch.LongTensor(indexed).to(device) # seq_len
tensor = tensor.unsqueeze(0) # batch_size* seq_len
mask = tensor == PAD_IDX
# print(tensor, "\n", mask)
with torch.no_grad():
pred = torch.sigmoid(model(tensor, mask))
return pred.item()

In [88]:

1
predict_sentiment(model, "这个酒店非常脏乱差,不推荐")

Out[88]:

1
0.6831367611885071

In [90]:

1
predict_sentiment(model, "这个酒店非常好,强烈推荐!")

Out[90]:

1
0.8252924680709839

In [91]:

1
predict_sentiment(model, "房间设备太破,连喷头都是不好用,空调几乎感觉不到,虽然我开了最大另外就是设备维修不及时,洗澡用品感觉都是廉价货,味道很奇怪的洗头液等等...总体感觉服务还可以,设备招待所水平...")

Out[91]:

1
0.5120517611503601

In [92]:

1
predict_sentiment(model, "房间稍小,但清洁,非常实惠。不足之处是:双人房的洗澡用品只有一套.宾馆反馈2008年8月5日:尊敬的宾客:您好!感谢您选择入住金陵溧阳宾馆!对于酒店双人房内的洗漱用品只有一套的问题,我们已经召集酒店相关部门对此问题进行了研究和整改。努力将我们的管理与服务工作做到位,进一步关注宾客,关注细节!再次向您表示我们最衷心的感谢!期待您能再次来溧阳并入住金陵溧阳宾馆!让我们有给您提供更加优质服务的机会!顺祝您工作顺利!身体健康!金陵溧阳宾馆客务关系主任")

Out[92]:

1
0.7319579124450684

In [93]:

1
predict_sentiment(model, "该酒店对去溧阳公务或旅游的人都很适合,自助早餐很丰富,酒店内部环境和服务很好。唯一的不足是酒店大门口在晚上时太乱,各种车辆和人在门口挤成一团。补充点评2008年5月9日:房间淋浴水压不稳,一会热、一会冷,很不好调整。宾馆反馈2008年5月13日:非常感谢您选择入住金陵溧阳宾馆。您给予我们的肯定与赞赏让我们倍受鼓舞,也使我们更加自信地去做好每一天的服务工作。正是有许多像您一样的宾客给予我们不断的鼓励和赞赏,酒店的服务品质才能得以不断提升。对于酒店大门口的秩序和房间淋浴水的问题我们已做出了相应的措施。再次向您表示我们最衷心的感谢!我们期待您的再次光临!")

Out[93]:

1
0.793725311756134

In [94]:

1
predict_sentiment(model, "环境不错,室内色调很温馨,MM很满意!就是窗户收拾得太马虎了,拉开窗帘就觉得很凌乱的感觉。最不足的地方就是淋浴了,一是地方太小了,二是洗澡时水时大时小的,中间还停了几秒!!")

Out[94]:

1
0.7605408430099487

In [95]:

1
2
3
model.load_state_dict(torch.load("wordavg-model.pth"))
test_loss, test_acc = evaluate(model, test_batches, test_label_batches, crit)
print("CNN model test loss: ", test_loss, "accuracy:", test_acc)
1
CNN model test loss:  0.44893962796897346 accuracy: 0.8133848134615247

RNN模型

  • 下面我们尝试把模型换成一个

    recurrent neural network

(RNN)。RNN经常会被用来encode一个sequence

ℎ𝑡=RNN(𝑥𝑡,ℎ𝑡−1)ht=RNN(xt,ht−1)

  • 我们使用最后一个hidden state ℎ𝑇hT来表示整个句子。

  • 然后我们把ℎ𝑇hT通过一个线性变换𝑓f,然后用来预测句子的情感。

img

img

In [57]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class RNNModel(nn.Module):
def __init__(self, vocab_size, embedding_size, output_size, pad_idx, hidden_size, dropout, avg_hidden=True):
super(RNNModel, self).__init__()
self.embed = nn.Embedding(vocab_size, embedding_size, padding_idx=pad_idx)
self.lstm = nn.LSTM(embedding_size, hidden_size, bidirectional=True, num_layers=2, batch_first=True)
self.linear = nn.Linear(hidden_size*2, output_size)

self.dropout = nn.Dropout(dropout)
self.avg_hidden = avg_hidden

def forward(self, text, mask):
embedded = self.embed(text) # [batch_size, seq_len, embedding_size] 其中包含一些pad
embedded = self.dropout(embedded)

# mask: batch_size * seq_length
seq_length = (1. - mask.float()).sum(1)
embedded = torch.nn.utils.rnn.pack_padded_sequence(
input=embedded,
lengths=seq_length,
batch_first=True,
enforce_sorted=False
) # batch_size * seq_len * ..., seq_len * batch_size * ...
output, (hidden, cell) = self.lstm(embedded)
output, seq_length = torch.nn.utils.rnn.pad_packed_sequence(
sequence=output,
batch_first=True,
padding_value=0,
total_length=mask.shape[1]
)

# output: [batch_size, seq_length, hidden_dim * num_directions]
# hidden: [num_layers * num_directions, batch_size, hidden_dim]


if self.avg_hidden:
hidden = torch.sum(output * (1. - mask.float()).unsqueeze(2), 1) / torch.sum((1. - mask.float()), 1).unsqueeze(1)
else:
# 拿最后一个hidden state作为句子的表示
# hidden: 2 * batch_size * hidden_size
hidden = torch.cat([hidden[-1], hidden[-2]], dim=1)
hidden = self.dropout(hidden.squeeze())
return self.linear(hidden)

In [58]:

1
2
3
4
5
6
model = RNNModel(vocab_size=VOCAB_SIZE, 
embedding_size=EMBEDDING_SIZE,
output_size=OUTPUT_SIZE,
pad_idx=PAD_IDX,
hidden_size=100,
dropout=0.5)

训练RNN模型

In [59]:

1
2
3
4
5
optimizer = torch.optim.Adam(model.parameters()) # L2
crit = nn.BCEWithLogitsLoss()

model = model.to(device)
crit = crit.to(device)

In [60]:

1
2
3
4
5
6
7
8
9
10
11
12
N_EPOCHS = 10
best_valid_acc = 0.
for epoch in range(N_EPOCHS):
train_loss, train_acc = train(model, train_batches, train_label_batches, optimizer, crit)
valid_loss, valid_acc = evaluate(model, dev_batches, dev_label_batches, crit)

if valid_acc > best_valid_acc:
best_valid_acc = valid_acc
torch.save(model.state_dict(), "lstm-model.pth")

print("Epoch", epoch, "Train Loss", train_loss, "Train Acc", train_acc)
print("Epoch", epoch, "Valid Loss", valid_loss, "Valid Acc", valid_acc)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Epoch 0 Train Loss 0.5140281977456996 Train Acc 0.7472633612363168
Epoch 0 Valid Loss 0.7321655497894631 Valid Acc 0.8133848134615247
Epoch 1 Train Loss 0.4205178504441526 Train Acc 0.8209916291049582
Epoch 1 Valid Loss 0.5658483397086155 Valid Acc 0.8391248392782616
Epoch 2 Train Loss 0.3576773465620036 Train Acc 0.8473921442369607
Epoch 2 Valid Loss 0.6089477152437777 Valid Acc 0.8545688548756996
Epoch 3 Train Loss 0.3190276817504391 Train Acc 0.8647778493238892
Epoch 3 Valid Loss 0.5731698980355968 Valid Acc 0.8622908625977073
Epoch 4 Train Loss 0.2850390273336434 Train Acc 0.8881197681905988
Epoch 4 Valid Loss 0.6073675444073966 Valid Acc 0.8622908625209961
Epoch 5 Train Loss 0.26827128295812463 Train Acc 0.8884417256922086
Epoch 5 Valid Loss 0.4971172449057934 Valid Acc 0.8700128701662925
Epoch 6 Train Loss 0.23699480644442233 Train Acc 0.9059884095299421
Epoch 6 Valid Loss 0.5370476412343549 Valid Acc 0.8635778636545748
Epoch 7 Train Loss 0.22414902945487483 Train Acc 0.9072762395363811
Epoch 7 Valid Loss 0.48257371315317876 Valid Acc 0.8725868726635838
Epoch 8 Train Loss 0.2119196125996435 Train Acc 0.9162910495814552
Epoch 8 Valid Loss 0.59562370292315 Valid Acc 0.8468468471536919
Epoch 9 Train Loss 0.20756761220698194 Train Acc 0.9207984546039922
Epoch 9 Valid Loss 0.6451035161122699 Valid Acc 0.8700128701662925

In [62]:

1
predict_sentiment(model, "沈阳市政府的酒店,比较大气,交通便利,出门往左就是北陵公园,环境好。")

Out[62]:

1
0.9994519352912903

In [63]:

1
predict_sentiment(model, "这个酒店非常脏乱差,不推荐!")

Out[63]:

1
0.01588270254433155

In [68]:

1
predict_sentiment(model, "这个酒店不乱,非常推荐!")

Out[68]:

1
0.04462616145610809

在test上做模型预测

In [69]:

1
2
3
model.load_state_dict(torch.load("lstm-model.pth"))
test_loss, test_acc = evaluate(model, test_batches, test_label_batches, crit)
print("CNN model test loss: ", test_loss, "accuracy:", test_acc)
1
CNN model test loss:  0.5639284941220376 accuracy: 0.8481338484406932

CNN模型

In [70]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class CNN(nn.Module):
def __init__(self, vocab_size, embedding_size, output_size, pad_idx, num_filters, filter_sizes, dropout):
super(CNN, self).__init__()
self.filter_sizes = filter_sizes
self.embed = nn.Embedding(vocab_size, embedding_size, padding_idx=pad_idx)
self.convs = nn.ModuleList([
nn.Conv2d(in_channels=1, out_channels=num_filters,
kernel_size=(fs, embedding_size))
for fs in filter_sizes
]) # 3个CNN
# fs实际上就是n-gram的n
# self.conv = nn.Conv2d(in_channels=1, out_channels=num_filters, kernel_size=(filter_size, embedding_size))
self.linear = nn.Linear(num_filters * len(filter_sizes), output_size)
self.dropout = nn.Dropout(dropout)

def forward(self, text, mask):
embedded = self.embed(text) # [batch_size, seq_len, embedding_size]
embedded = embedded.unsqueeze(1) # # [batch_size, 1, seq_len, embedding_size]
# conved = F.relu(self.conv(embedded)) # [batch_size, num_filters, seq_len-filter_size+1, 1]
# conved = conved.squeeze(3) # [batch_size, num_filters, seq_len-filter_size+1]
conved = [
F.relu(conv(embedded)).squeeze(3) for conv in self.convs
] # [batch_size, num_filters, seq_len-filter_size+1]

# [2, 5, 1, 1]

# mask [[0, 0, 1, 1]]
# fs: 2
# [0, 0, 1]
conved = [
conv.masked_fill(mask[:, :-filter_size+1].unsqueeze(1) , -999999) for (conv, filter_size) in zip(conved, self.filter_sizes)
]
# max over time pooling
# pooled = F.max_pool1d(conved, conved.shape[2]) # [batch_size, num_filters, 1]
# pooled = pooled.squeeze(2)
pooled = [F.max_pool1d(conv, conv.shape[2]).squeeze(2) for conv in conved]
pooled = torch.cat(pooled, dim=1) # batch_size, 3*num_filters
pooled = self.dropout(pooled)

return self.linear(pooled)

# Conv1d? 1x1 Conv2d?

In [71]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
model = CNN(vocab_size=VOCAB_SIZE, 
embedding_size=EMBEDDING_SIZE,
output_size=OUTPUT_SIZE,
pad_idx=PAD_IDX,
num_filters=100,
filter_sizes=[3,4,5], # 3-gram, 4-gram, 5-gram
dropout=0.5)

optimizer = torch.optim.Adam(model.parameters())
crit = nn.BCEWithLogitsLoss()

model = model.to(device)
crit = crit.to(device)

N_EPOCHS = 10
best_valid_acc = 0.
for epoch in range(N_EPOCHS):
train_loss, train_acc = train(model, train_batches, train_label_batches, optimizer, crit)
valid_loss, valid_acc = evaluate(model, dev_batches, dev_label_batches, crit)

if valid_acc > best_valid_acc:
best_valid_acc = valid_acc
torch.save(model.state_dict(), "cnn-model.pth")

print("Epoch", epoch, "Train Loss", train_loss, "Train Acc", train_acc)
print("Epoch", epoch, "Valid Loss", valid_loss, "Valid Acc", valid_acc)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Epoch 0 Train Loss 0.5229088294452341 Train Acc 0.7443657437218287
Epoch 0 Valid Loss 0.39319338566087847 Valid Acc 0.8108108110409445
Epoch 1 Train Loss 0.3683148011498043 Train Acc 0.837894397939472
Epoch 1 Valid Loss 0.3534783678778964 Valid Acc 0.840411840565263
Epoch 2 Train Loss 0.3185185533801318 Train Acc 0.8644558918222794
Epoch 2 Valid Loss 0.34023444222207233 Valid Acc 0.8545688547222771
Epoch 3 Train Loss 0.27130810793366883 Train Acc 0.8889246619446233
Epoch 3 Valid Loss 0.30879392936116173 Valid Acc 0.8648648650182874
Epoch 4 Train Loss 0.24334710945314694 Train Acc 0.9034127495170637
Epoch 4 Valid Loss 0.3020249246553718 Valid Acc 0.8790218791753015
Epoch 5 Train Loss 0.2156534520195556 Train Acc 0.912105602060528
Epoch 5 Valid Loss 0.326562241774575 Valid Acc 0.8571428572962797
Epoch 6 Train Loss 0.189559489642123 Train Acc 0.9245009658725049
Epoch 6 Valid Loss 0.28917587651095644 Valid Acc 0.885456885610308
Epoch 7 Train Loss 0.16508568145445063 Train Acc 0.9356084996780425
Epoch 7 Valid Loss 0.2982815937876241 Valid Acc 0.8790218791753015
Epoch 8 Train Loss 0.14198238390007764 Train Acc 0.9452672247263362
Epoch 8 Valid Loss 0.2929042390184513 Valid Acc 0.8880308881843105
Epoch 9 Train Loss 0.11862559608529824 Train Acc 0.9552479072762395
Epoch 9 Valid Loss 0.29382622203618247 Valid Acc 0.886743886820598

In [72]:

1
2
3
model.load_state_dict(torch.load("cnn-model.pth"))
test_loss, test_acc = evaluate(model, test_batches, test_label_batches, crit)
print("CNN model test loss: ", test_loss, "accuracy:", test_acc)
1
CNN model test loss:  0.32514461861537386 accuracy: 0.8674388674388674

In [74]:

1
predict_sentiment(model, "酒店位于昆明中心区,地理位置不错,可惜酒店服务有些差,第一天晚上可能入住的客人不多,空调根本没开,打了电话问,说是中央空调要晚上统一开,结果晚上也没开,就热了一晚上,第二天有开会的入住,晚上就有了空调,不得不说酒店经济帐作的好.房间的床太硬,睡的不好.酒店的早餐就如其他人评价一样,想法的难吃.不过携程的预订价钱还不错.")

Out[74]:

1
0.893503725528717

learning representation

1
 

NLP技术基础整理

什么是自然语言处理?

自然语言处理(NLP)是一门融语言学、计算机科学、人工智能于一体的(实验性)科学,解决的是“让机器可以理解自然语言”。

NLP = NLU + NLG

NLP问题的难点

  • 自然语言有歧义(ambiguity),同样的含义又有不同的表达方式(variability)
    • ambiguity:同样的一段表述能表示不同的意思
    • variability:不同的表达方式是同一个意思

coreference resolution

爸爸已经抱不动小明了,因为太胖了。

爸爸已经抱不动小明了,因为太虚弱了。

WSC: GPT-2

机器学习与NLP

使用机器学习的方法让模型能够学到输入和输出之间的映射关系。在NLP中,输入一般都是语言文字,而输出则是各种不同的label。

单词

自然语言的基本构成单元。

分词

英文中的单词一般用空格隔开(标点符号等特殊情况除外),所以天然地完成了分词。中文的分词则不那么自然,需要人为分词。比较好用的分词工具:https://github.com/lancopku/pkuseg-python

jieba

1
2
3
4
5
6
7
pip install pkuseg
>>> import pkuseg
>>>
>>> seg = pkuseg.pkuseg() # 以默认配置加载模型
>>> text = seg.cut('我爱北京天安门') # 进行分词
>>> print(text)
['我', '爱', '北京', '天安门']

英文分词可以使用NLTK

1
2
3
4
5
6
>>> import nltk
>>> sentence = “hello, world"
>>> tokens = nltk.word_tokenize(sentence)
>>> tokens
['hello', ‘,', 'world']
>>> sents = nltk.sent_tokenize(documents)

NLTK还有一些好用的功能,例如POS Tagging

1
2
3
4
5
6
>>> import nltk
>>> text = nltk.word_tokenize('what does the fox say')
>>> text
['what', 'does', 'the', 'fox', 'say']
>>> nltk.pos_tag(text)
[('what', 'WDT'), ('does', 'VBZ'), ('the', 'DT'), ('fox', 'NNS'), ('say', 'VBP')]

Named Entity Recognition

img

去除停用词

1
2
3
4
5
6
from nltk.corpus import stopwords
# 先token一把,得到一个word_list
# ...
# 然后filter一把
filtered_words =
[word for word in word_list if word not in stopwords.words('english')]

one hot vector [0, 0, 0, 1, 0, 0…]

Bag of Words和TF-IDF

词包模型

vocab: 50000个单词

文本–> 50000维向量

{a: 0, an: 1, the:2, ….}

[100, 50, 30, …]

TF: Term Frequency, 衡量一个term在文档中出现得有多频繁。

TF(t) = (t出现在文档中的次数) / (文档中的term总数)

文档一个10000个单词,100个the

TF(the) = 0.01

IDF: Inverse Document Frequency, 衡量一个term有多重要。

有些词出现的很多,但是信息量可能不大,比如’is’,’the‘,’and‘之类。

为了平衡,我们把罕见的词的重要性(weight)调高,把常见词的重要性调低。

IDF(t) = lg(文档总数 / 含有t的文档总数 + 1)

语料一共在3篇文章中出现,但是我们一共有100,000篇文章。IDF(julyedu) = log(100,000/3)

TF-IDF = TF * IDF

TFIDF词包

a, 100*0.000001

[0.0001, ]

Distributional Word Vectors 词向量

distributional semantics

“The distributional hypothesis in linguistics is derived from the semantic theory of language usage, i.e. words that are used and occur in the same contexts tend to purport similar meanings.”

如果两个单词总是在同样的语境下出现,那么表示他们之间存在某种相关性/相似性。

Counting Context Words

img

50000 * 50000

50000*300

300*300

300*50000

在我们定义的固定大小的context window下出现的单词组,就是co-occuring word pairs。

对于句子的开头和结尾,我们可以定义两个特殊的符号 <s> 和 </s>。

单词相似度

使用词向量间的cosine相似度(cosine 夹角), u, v是两个词向量

img= cosine(u, v)

单词”cooked”周围context windows最常见的单词

img

Pointwise Mutual Information (PMI)

img

独立 P(x)*P(y) = P(x, y)

PMI表示了事件 x 和事件 y 之间是否存在相关性。

与 “cooked” PMI值最高的单词

img

如何评估词向量的好坏?

标准化的单词相似度数据集

  • Assign a numerical similarity score between 0 and 10 (0 = words are totally unrelated, 10 = words are VERY closely related).

imgimg

cosine(journey, voyage) =

cosine(king, queen) =

spearman’s R 分数

Sparse vs. dense vectors

根据context window定义的词向量非常长,很多位置上都是0. 表示我们的信息密度是很低的

  • 低维度词向量更容易训练模型,占用的内存/硬盘也会比较小。

  • 低维度词向量能够学到一些单词间的关系,例如有些单词之间是近义词。

降维算法

  • PCA

  • SVD

  • Brown cluster

  • Word2Vec

Contextualized Word Vectors

近两年非常流行的做法,不仅仅是针对单个单词训练词向量,而是根据单词出现的语境给出词向量,是的该词向量既包含当前单词的信息,又包含单词周围context的信息。

  • BERT, RoBERTa, ALBERT, T5

  • GPT2

文本分类

NLP数据集

  • NLP数据集一般包含输入(inputs,一般是文字)和输出(outputs,一般是某种标注)。

标注

  • 监督学习需要标注过的数据集,这些标注一般被称为ground truth。

  • 在自然语言处理数据集中,标注往往是由人手动标注的

  • 人们往往会对数据的标注有不同的意见,因为很多时候不同的人对同样的语言会有不同的理解。所以我们也会把这些标注称为gold standard,而不是ground truth。

NLP数据集如何构建

  • 付钱请人标注

    • 比较传统的做法
    • 研究员写下标注的guideline,然后花钱请人标注(以前一般请一些专业的语言学家)
    • 标注的质量会比较高,但是成本也高
    • 例如,Penn Treebank(1993)
  • Croudsourcing

    • 现在比较流行
    • 一般不专门训练标注者(annotator),但是可以对同一条数据取得多条样本
    • 例如,Stanford Sentiment Treebank
  • 自然拥有标注的数据集

    • 法律文件的中英文版本,可以用于训练翻译模型
    • 报纸的内容分类
    • 聊天记录
    • 文本摘要(新闻的全文和摘要)

标注者同意度 Annotator Agreement

  • 给定两个标注者给出的所有标注,如何计算他们之间的标注是否比较统一?
    • 相同标注的百分比?
    • Cohen’s Kappa

img

来自维基百科

  • 也有更多别的测量方法

常见的文本分类数据集

英文:

中文:

文本分类模型

什么是一个分类器?

  • 一个从输入(inputs)特征x投射到标注y的函数

  • 一个简单的分类器:

    • 对于输入x,给每一个label y打一个分数,score(x, y, w),其中w是模型的参数
    • 分类问题也就是选出分数最高的y:classify(x, w) = argmax_y score(x, y, w)

Modeling, Inference, Learning

img

Modeling

二元情感分类

classify(x, w) = argmax_y score(x, y, w)

如果我们采用线性模型,那么模型可以被写为

img

现在的问题是,我们如何定义f?对于比较常见的机器学习问题,我们的输入往往是格式固定的,但是NLP的输入一般是长度不固定的文本。这里就涉及到如何把文本转换成特征(feature)。

  • 过去25年:特征工程(feature engineering),人为定制,比较复杂,只适用于某一类问题

  • 过去5年:表示学习(representation learning), ICLR, international conference for learning representations

常见的features:

f1: 文本是正面情感,文本包含“好”

f2: 文本是负面情感,文本包含“好”

。。。

Inference

比较直观,给定一段话,在每个Label上打分,然后取分数最大的label作为预测。

Learning

  • 根据训练数据得到模型权重w

  • 把数据分为训练集(train),验证集(dev, val)测试集(test)

  • 在NLP中,我们常常使用一种learning framework: Empirical Risk Minimization

    • 损失函数(cost function):对比模型的预测和gold standard,计算一个分数img
    • 损失函数与我们真正优化的目标要尽量保持一致
    • 一般来说如果cost为0,表示我们的模型预测完全正确
    • 对于文本分类来说,我们应该使用怎样的损失函数呢?

错误率:img

Risk Minimization:

给定训练数据 imgx表示输入,y表示label

我们的目标是img

Empirical Risk Minimization img

我们之前定义的0-1损失函数是很难优化的,因为0-1loss不连续,所以无法使用基于梯度的优化方法。

loss.backward() # \d loss / \d w = gradient

optimizer.step() # w - learning_rate*gradient

cost = -score(x, y_label, w) 问题:没有考虑到label之间的关系!

一些其他的损失函数

perceptron loss

img

hinge loss

img

img

Log Loss/Cross Entropy Loss

img

我们之前只有score(x, y, w),怎么样定义p_w(y | z)

  • 让gold standard label的条件概率尽可能大

  • 使用softmax把score转化成概率

  • 其中的score function可以是各种函数,例如一个神经网络

损失函数往往会结合regularization 正则项

  • L2 regularization img

  • L1 regularization img

模型训练

  • (stochastic, batch) gradient descent

语言模型

语言模型:给句子计算一个概率

为什么会有这样一个奇怪的任务?

  • 机器翻译:P(我喜欢吃水果)> P(我喜欢喝水果)

  • 拼写检查:P(我想吃饭)> P(我像吃饭)

  • 语音识别:P (我看见了一架飞机)> P(我看见了一架斐济)

  • summarizaton, question answering, etc.

文本自动补全。。。

概率语言模型(probablistic language modeling)

  • 目标:计算一串单词连成一个句子的概率 P(w) = P(w_1, …, w_n)

  • 相关的任务 P(w_4|w_1, …, w_3)

  • 这两个任务的模型都称之为语言模型

条件概率

img

马尔科夫假设

  • 上述条件概率公式只取决于最近的n-1个单词 P(w_i|w_1, …, w_{i-1}) = P(w_i | w_{i-n+1}, …, w_{i-1})

  • 我们创建出了n-gram模型

  • 简单的案例,bigram模型

img

一些Smoothing方法

  • “Add-1” estimation

img

  • Backoff,如果一些trigram存在,就是用trigram,如果不存在,就是用bigram,如果bigram也不存在,就退而求其次使用unigram。

  • interpolation:混合使用unigram, bigram, trigram

Perplexity: 用于评估语言模型的好坏。评估的语言,我现在给你一套比较好的语言,我希望自己的语言模型能够给这段话尽可能高的分数。

img l 越大越好,-l 越小越好

imgPP 越小越好 困惑度

perplexity越低 = 模型越好

简单的Trigram神经网络语言模型

img

这个模型可以使用log loss来训练。

我们还可以在这个模型的基础上增加hidden layer

img

循环神经网络(Recurrent Neural Networks)

img

基于循环神经网络的语言模型

img

img

Long Short-term Memory

img

Gates

img

Gated Recurrent Unit (GRU)

img

https://colah.github.io/posts/2015-08-Understanding-LSTMs/

Word2Vec

img

我们的目标是利用没有标注过的纯文本训练有用的词向量(word vectors)

skip-gram (window size = 5)

agriculture is the tradional mainstay of the cambodian economy . but benares has been destroyed by an earthquake.

img

skip-gram中使用的score function

img

模型参数,所有的单词的词向量,包括输入向量和输出向量

learning

img

img

注意这个概率模型需要汇总单词表中的所有单词,计算量非常之大

Negative Sampling

img

随机生成一些负例,然后优化以上损失函数

img

CNN-Image-Classification

1
2
3
4
5
6
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms
print("PyTorch Version: ",torch.__version__)
1
PyTorch Version:  1.0.0

首先我们定义一个基于ConvNet的简单神经网络

In [4]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(1, 20, 5, 1)
self.conv2 = nn.Conv2d(20, 50, 5, 1)
self.fc1 = nn.Linear(4*4*50, 500)
self.fc2 = nn.Linear(500, 10)

def forward(self, x):
x = F.relu(self.conv1(x))
x = F.max_pool2d(x, 2, 2)
x = F.relu(self.conv2(x))
x = F.max_pool2d(x, 2, 2)
x = x.view(-1, 4*4*50)
x = F.relu(self.fc1(x))
x = self.fc2(x)
return F.log_softmax(x, dim=1)

NLL loss的定义

ℓ(𝑥,𝑦)=𝐿={𝑙1,…,𝑙𝑁}⊤,𝑙𝑛=−𝑤𝑦𝑛𝑥𝑛,𝑦𝑛,𝑤𝑐=weight[𝑐]⋅𝟙{𝑐≠ignore_index}ℓ(x,y)=L={l1,…,lN}⊤,ln=−wynxn,yn,wc=weight[c]⋅1{c≠ignore_index}

In [7]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def train(model, device, train_loader, optimizer, epoch, log_interval=100):
model.train()
for batch_idx, (data, target) in enumerate(train_loader):
data, target = data.to(device), target.to(device)
optimizer.zero_grad()
output = model(data)
loss = F.nll_loss(output, target)
loss.backward()
optimizer.step()
if batch_idx % log_interval == 0:
print("Train Epoch: {} [{}/{} ({:0f}%)]\tLoss: {:.6f}".format(
epoch, batch_idx * len(data), len(train_loader.dataset),
100. * batch_idx / len(train_loader), loss.item()
))

In [8]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def test(model, device, test_loader):
model.eval()
test_loss = 0
correct = 0
with torch.no_grad():
for data, target in test_loader:
data, target = data.to(device), target.to(device)
output = model(data)
test_loss += F.nll_loss(output, target, reduction='sum').item() # sum up batch loss
pred = output.argmax(dim=1, keepdim=True) # get the index of the max log-probability
correct += pred.eq(target.view_as(pred)).sum().item()

test_loss /= len(test_loader.dataset)

print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
test_loss, correct, len(test_loader.dataset),
100. * correct / len(test_loader.dataset)))

In [13]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
torch.manual_seed(53113)

use_cuda = torch.cuda.is_available()
device = torch.device("cuda" if use_cuda else "cpu")
batch_size = test_batch_size = 32
kwargs = {'num_workers': 1, 'pin_memory': True} if use_cuda else {}
train_loader = torch.utils.data.DataLoader(
datasets.MNIST('./mnist_data', train=True, download=True,
transform=transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])),
batch_size=batch_size, shuffle=True, **kwargs)
test_loader = torch.utils.data.DataLoader(
datasets.MNIST('./mnist_data', train=False, transform=transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])),
batch_size=test_batch_size, shuffle=True, **kwargs)


lr = 0.01
momentum = 0.5
model = Net().to(device)
optimizer = optim.SGD(model.parameters(), lr=lr, momentum=momentum)

epochs = 2
for epoch in range(1, epochs + 1):
train(model, device, train_loader, optimizer, epoch)
test(model, device, test_loader)

save_model = True
if (save_model):
torch.save(model.state_dict(),"mnist_cnn.pt")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
Train Epoch: 1 [0/60000 (0.000000%)]	Loss: 2.297938
Train Epoch: 1 [3200/60000 (5.333333%)] Loss: 0.567845
Train Epoch: 1 [6400/60000 (10.666667%)] Loss: 0.206370
Train Epoch: 1 [9600/60000 (16.000000%)] Loss: 0.094653
Train Epoch: 1 [12800/60000 (21.333333%)] Loss: 0.180530
Train Epoch: 1 [16000/60000 (26.666667%)] Loss: 0.041645
Train Epoch: 1 [19200/60000 (32.000000%)] Loss: 0.135092
Train Epoch: 1 [22400/60000 (37.333333%)] Loss: 0.054001
Train Epoch: 1 [25600/60000 (42.666667%)] Loss: 0.111863
Train Epoch: 1 [28800/60000 (48.000000%)] Loss: 0.059039
Train Epoch: 1 [32000/60000 (53.333333%)] Loss: 0.089227
Train Epoch: 1 [35200/60000 (58.666667%)] Loss: 0.186015
Train Epoch: 1 [38400/60000 (64.000000%)] Loss: 0.093208
Train Epoch: 1 [41600/60000 (69.333333%)] Loss: 0.077090
Train Epoch: 1 [44800/60000 (74.666667%)] Loss: 0.038075
Train Epoch: 1 [48000/60000 (80.000000%)] Loss: 0.036247
Train Epoch: 1 [51200/60000 (85.333333%)] Loss: 0.052358
Train Epoch: 1 [54400/60000 (90.666667%)] Loss: 0.013201
Train Epoch: 1 [57600/60000 (96.000000%)] Loss: 0.036660

Test set: Average loss: 0.0644, Accuracy: 9802/10000 (98%)

Train Epoch: 2 [0/60000 (0.000000%)] Loss: 0.054402
Train Epoch: 2 [3200/60000 (5.333333%)] Loss: 0.032239
Train Epoch: 2 [6400/60000 (10.666667%)] Loss: 0.092350
Train Epoch: 2 [9600/60000 (16.000000%)] Loss: 0.058544
Train Epoch: 2 [12800/60000 (21.333333%)] Loss: 0.029762
Train Epoch: 2 [16000/60000 (26.666667%)] Loss: 0.012521
Train Epoch: 2 [19200/60000 (32.000000%)] Loss: 0.101891
Train Epoch: 2 [22400/60000 (37.333333%)] Loss: 0.127773
Train Epoch: 2 [25600/60000 (42.666667%)] Loss: 0.009259
Train Epoch: 2 [28800/60000 (48.000000%)] Loss: 0.013482
Train Epoch: 2 [32000/60000 (53.333333%)] Loss: 0.039676
Train Epoch: 2 [35200/60000 (58.666667%)] Loss: 0.016707
Train Epoch: 2 [38400/60000 (64.000000%)] Loss: 0.168691
Train Epoch: 2 [41600/60000 (69.333333%)] Loss: 0.056318
Train Epoch: 2 [44800/60000 (74.666667%)] Loss: 0.008174
Train Epoch: 2 [48000/60000 (80.000000%)] Loss: 0.075149
Train Epoch: 2 [51200/60000 (85.333333%)] Loss: 0.205798
Train Epoch: 2 [54400/60000 (90.666667%)] Loss: 0.019762
Train Epoch: 2 [57600/60000 (96.000000%)] Loss: 0.012056

Test set: Average loss: 0.0464, Accuracy: 9850/10000 (98%)

In [15]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
torch.manual_seed(53113)

use_cuda = torch.cuda.is_available()
device = torch.device("cuda" if use_cuda else "cpu")
batch_size = test_batch_size = 32
kwargs = {'num_workers': 1, 'pin_memory': True} if use_cuda else {}
train_loader = torch.utils.data.DataLoader(
datasets.FashionMNIST('./fashion_mnist_data', train=True, download=True,
transform=transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])),
batch_size=batch_size, shuffle=True, **kwargs)
test_loader = torch.utils.data.DataLoader(
datasets.FashionMNIST('./fashion_mnist_data', train=False, transform=transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])),
batch_size=test_batch_size, shuffle=True, **kwargs)


lr = 0.01
momentum = 0.5
model = Net().to(device)
optimizer = optim.SGD(model.parameters(), lr=lr, momentum=momentum)

epochs = 2
for epoch in range(1, epochs + 1):
train(model, device, train_loader, optimizer, epoch)
test(model, device, test_loader)

save_model = True
if (save_model):
torch.save(model.state_dict(),"fashion_mnist_cnn.pt")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
Downloading http://fashion-mnist.s3-website.eu-central-1.amazonaws.com/train-images-idx3-ubyte.gz
Downloading http://fashion-mnist.s3-website.eu-central-1.amazonaws.com/train-labels-idx1-ubyte.gz
Downloading http://fashion-mnist.s3-website.eu-central-1.amazonaws.com/t10k-images-idx3-ubyte.gz
Downloading http://fashion-mnist.s3-website.eu-central-1.amazonaws.com/t10k-labels-idx1-ubyte.gz
Processing...
Done!
Train Epoch: 1 [0/60000 (0.000000%)] Loss: 2.279603
Train Epoch: 1 [3200/60000 (5.333333%)] Loss: 0.962251
Train Epoch: 1 [6400/60000 (10.666667%)] Loss: 1.019635
Train Epoch: 1 [9600/60000 (16.000000%)] Loss: 0.544330
Train Epoch: 1 [12800/60000 (21.333333%)] Loss: 0.629807
Train Epoch: 1 [16000/60000 (26.666667%)] Loss: 0.514437
Train Epoch: 1 [19200/60000 (32.000000%)] Loss: 0.555741
Train Epoch: 1 [22400/60000 (37.333333%)] Loss: 0.528186
Train Epoch: 1 [25600/60000 (42.666667%)] Loss: 0.656440
Train Epoch: 1 [28800/60000 (48.000000%)] Loss: 0.294654
Train Epoch: 1 [32000/60000 (53.333333%)] Loss: 0.293626
Train Epoch: 1 [35200/60000 (58.666667%)] Loss: 0.227645
Train Epoch: 1 [38400/60000 (64.000000%)] Loss: 0.473842
Train Epoch: 1 [41600/60000 (69.333333%)] Loss: 0.724678
Train Epoch: 1 [44800/60000 (74.666667%)] Loss: 0.519580
Train Epoch: 1 [48000/60000 (80.000000%)] Loss: 0.465854
Train Epoch: 1 [51200/60000 (85.333333%)] Loss: 0.378200
Train Epoch: 1 [54400/60000 (90.666667%)] Loss: 0.503832
Train Epoch: 1 [57600/60000 (96.000000%)] Loss: 0.616502

Test set: Average loss: 0.4365, Accuracy: 8425/10000 (84%)

Train Epoch: 2 [0/60000 (0.000000%)] Loss: 0.385171
Train Epoch: 2 [3200/60000 (5.333333%)] Loss: 0.329045
Train Epoch: 2 [6400/60000 (10.666667%)] Loss: 0.308792
Train Epoch: 2 [9600/60000 (16.000000%)] Loss: 0.360471
Train Epoch: 2 [12800/60000 (21.333333%)] Loss: 0.445865
Train Epoch: 2 [16000/60000 (26.666667%)] Loss: 0.357145
Train Epoch: 2 [19200/60000 (32.000000%)] Loss: 0.376523
Train Epoch: 2 [22400/60000 (37.333333%)] Loss: 0.389735
Train Epoch: 2 [25600/60000 (42.666667%)] Loss: 0.308655
Train Epoch: 2 [28800/60000 (48.000000%)] Loss: 0.352300
Train Epoch: 2 [32000/60000 (53.333333%)] Loss: 0.499613
Train Epoch: 2 [35200/60000 (58.666667%)] Loss: 0.282398
Train Epoch: 2 [38400/60000 (64.000000%)] Loss: 0.330232
Train Epoch: 2 [41600/60000 (69.333333%)] Loss: 0.430427
Train Epoch: 2 [44800/60000 (74.666667%)] Loss: 0.406084
Train Epoch: 2 [48000/60000 (80.000000%)] Loss: 0.443538
Train Epoch: 2 [51200/60000 (85.333333%)] Loss: 0.348947
Train Epoch: 2 [54400/60000 (90.666667%)] Loss: 0.424920
Train Epoch: 2 [57600/60000 (96.000000%)] Loss: 0.231494

Test set: Average loss: 0.3742, Accuracy: 8652/10000 (87%)

CNN模型的迁移学习

  • 很多时候当我们需要训练一个新的图像分类任务,我们不会完全从一个随机的模型开始训练,而是利用_预训练_的模型来加速训练的过程。我们经常使用在ImageNet上的预训练模型。
  • 这是一种transfer learning的方法。我们常用以下两种方法做迁移学习。
    • fine tuning: 从一个预训练模型开始,我们改变一些模型的架构,然后继续训练整个模型的参数。
    • feature extraction: 我们不再改变与训练模型的参数,而是只更新我们改变过的部分模型参数。我们之所以叫它feature extraction是因为我们把预训练的CNN模型当做一个特征提取模型,利用提取出来的特征做来完成我们的训练任务。

以下是构建和训练迁移学习模型的基本步骤:

  • 初始化预训练模型
  • 把最后一层的输出层改变成我们想要分的类别总数
  • 定义一个optimizer来更新参数
  • 模型训练

In [87]:

1
2
3
4
5
6
7
8
9
import numpy as np
import torchvision
from torchvision import datasets, transforms, models

import matplotlib.pyplot as plt
import time
import os
import copy
print("Torchvision Version: ",torchvision.__version__)
1
Torchvision Version:  0.2.0

数据

我们会使用hymenoptera_data数据集,下载.

这个数据集包括两类图片, beesants, 这些数据都被处理成了可以使用ImageFolder <https://pytorch.org/docs/stable/torchvision/datasets.html#torchvision.datasets.ImageFolder>来读取的格式。我们只需要把data_dir设置成数据的根目录,然后把model_name设置成我们想要使用的与训练模型: :: [resnet, alexnet, vgg, squeezenet, densenet, inception]

其他的参数有:

  • num_classes表示数据集分类的类别数
  • batch_size
  • num_epochs
  • feature_extract表示我们训练的时候使用fine tuning还是feature extraction方法。如果feature_extract = False,整个模型都会被同时更新。如果feature_extract = True,只有模型的最后一层被更新。

In [36]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Top level data directory. Here we assume the format of the directory conforms 
# to the ImageFolder structure
data_dir = "./hymenoptera_data"
# Models to choose from [resnet, alexnet, vgg, squeezenet, densenet, inception]
model_name = "resnet"
# Number of classes in the dataset
num_classes = 2
# Batch size for training (change depending on how much memory you have)
batch_size = 32
# Number of epochs to train for
num_epochs = 15
# Flag for feature extracting. When False, we finetune the whole model,
# when True we only update the reshaped layer params
feature_extract = True

In [120]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
def train_model(model, dataloaders, criterion, optimizer, num_epochs=5):
since = time.time()
val_acc_history = []
best_model_wts = copy.deepcopy(model.state_dict())
best_acc = 0.
for epoch in range(num_epochs):
print("Epoch {}/{}".format(epoch, num_epochs-1))
print("-"*10)

for phase in ["train", "val"]:
running_loss = 0.
running_corrects = 0.
if phase == "train":
model.train()
else:
model.eval()

for inputs, labels in dataloaders[phase]:
inputs = inputs.to(device)
labels = labels.to(device)

with torch.autograd.set_grad_enabled(phase=="train"):
outputs = model(inputs)
loss = criterion(outputs, labels)

_, preds = torch.max(outputs, 1)
if phase == "train":
optimizer.zero_grad()
loss.backward()
optimizer.step()

running_loss += loss.item() * inputs.size(0)
running_corrects += torch.sum(preds.view(-1) == labels.view(-1)).item()

epoch_loss = running_loss / len(dataloaders[phase].dataset)
epoch_acc = running_corrects / len(dataloaders[phase].dataset)

print("{} Loss: {} Acc: {}".format(phase, epoch_loss, epoch_acc))
if phase == "val" and epoch_acc > best_acc:
best_acc = epoch_acc
best_model_wts = copy.deepcopy(model.state_dict())
if phase == "val":
val_acc_history.append(epoch_acc)

print()

time_elapsed = time.time() - since
print("Training compete in {}m {}s".format(time_elapsed // 60, time_elapsed % 60))
print("Best val Acc: {}".format(best_acc))

model.load_state_dict(best_model_wts)
return model, val_acc_history

In [121]:

1
2
3
4
# it = iter(dataloaders_dict["train"])
# inputs, labels = next(it)
# for inputs, labels in dataloaders_dict["train"]:
# print(labels.size())

In [122]:

1
len(dataloaders_dict["train"].dataset.imgs)

Out[122]:

1
244

In [123]:

1
len(dataloaders_dict["train"].dataset)

Out[123]:

1
244

In [124]:

1
2
3
4
def set_parameter_requires_grad(model, feature_extracting):
if feature_extracting:
for param in model.parameters():
param.requires_grad = False

In [125]:

1
2
3
4
5
6
7
8
9
10
11
def initialize_model(model_name, num_classes, feature_extract, use_pretrained=True):
if model_name == "resnet":
model_ft = models.resnet18(pretrained=use_pretrained)
set_parameter_requires_grad(model_ft, feature_extract)
num_ftrs = model_ft.fc.in_features
model_ft.fc = nn.Linear(num_ftrs, num_classes)
input_size = 224

return model_ft, input_size
model_ft, input_size = initialize_model(model_name, num_classes, feature_extract, use_pretrained=True)
print(model_ft)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
ResNet(
(conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace)
(maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
(layer1): Sequential(
(0): BasicBlock(
(conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
(1): BasicBlock(
(conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(layer2): Sequential(
(0): BasicBlock(
(conv1): Conv2d(64, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(64, 128, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(layer3): Sequential(
(0): BasicBlock(
(conv1): Conv2d(128, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(128, 256, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(layer4): Sequential(
(0): BasicBlock(
(conv1): Conv2d(256, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(downsample): Sequential(
(0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): BasicBlock(
(conv1): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(avgpool): AvgPool2d(kernel_size=7, stride=1, padding=0)
(fc): Linear(in_features=512, out_features=2, bias=True)
)

读入数据

现在我们知道了模型输入的size,我们就可以把数据预处理成相应的格式。

In [126]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
data_transforms = {
"train": transforms.Compose([
transforms.RandomResizedCrop(input_size),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
]),
"val": transforms.Compose([
transforms.Resize(input_size),
transforms.CenterCrop(input_size),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
]),
}

print("Initializing Datasets and Dataloaders...")

# Create training and validation datasets
image_datasets = {x: datasets.ImageFolder(os.path.join(data_dir, x), data_transforms[x]) for x in ['train', 'val']}
# Create training and validation dataloaders
dataloaders_dict = {x: torch.utils.data.DataLoader(image_datasets[x], batch_size=batch_size, shuffle=True, num_workers=4) for x in ['train', 'val']}

# Detect if we have a GPU available
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
1
Initializing Datasets and Dataloaders...

In [ ]:

1
2


In [127]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Send the model to GPU
model_ft = model_ft.to(device)

# Gather the parameters to be optimized/updated in this run. If we are
# finetuning we will be updating all parameters. However, if we are
# doing feature extract method, we will only update the parameters
# that we have just initialized, i.e. the parameters with requires_grad
# is True.
params_to_update = model_ft.parameters()
print("Params to learn:")
if feature_extract:
params_to_update = []
for name,param in model_ft.named_parameters():
if param.requires_grad == True:
params_to_update.append(param)
print("\t",name)
else:
for name,param in model_ft.named_parameters():
if param.requires_grad == True:
print("\t",name)

# Observe that all parameters are being optimized
optimizer_ft = optim.SGD(params_to_update, lr=0.001, momentum=0.9)
1
2
3
Params to learn:
fc.weight
fc.bias

In [133]:

1
2
3
4
5
# Setup the loss fxn
criterion = nn.CrossEntropyLoss()

# Train and evaluate
model_ft, ohist = train_model(model_ft, dataloaders_dict, criterion, optimizer_ft, num_epochs=num_epochs)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
Epoch 0/14
----------
train Loss: 0.2623850886450439 Acc: 0.8975409836065574
val Loss: 0.22199168762350394 Acc: 0.9215686274509803

Epoch 1/14
----------
train Loss: 0.20775875546893136 Acc: 0.9262295081967213
val Loss: 0.21329789413930544 Acc: 0.9215686274509803

Epoch 2/14
----------
train Loss: 0.24463887243974405 Acc: 0.9098360655737705
val Loss: 0.2308054333613589 Acc: 0.9215686274509803

Epoch 3/14
----------
train Loss: 0.2108444703406975 Acc: 0.930327868852459
val Loss: 0.20637644174831365 Acc: 0.954248366013072

Epoch 4/14
----------
train Loss: 0.22102872954040279 Acc: 0.9221311475409836
val Loss: 0.19902625017695957 Acc: 0.9281045751633987

Epoch 5/14
----------
train Loss: 0.22044393127081824 Acc: 0.9221311475409836
val Loss: 0.2212505256818011 Acc: 0.9281045751633987

Epoch 6/14
----------
train Loss: 0.1636357441788814 Acc: 0.9467213114754098
val Loss: 0.1969745449380937 Acc: 0.934640522875817

Epoch 7/14
----------
train Loss: 0.1707800094221459 Acc: 0.9385245901639344
val Loss: 0.20569930824578977 Acc: 0.934640522875817

Epoch 8/14
----------
train Loss: 0.18224841185280535 Acc: 0.9344262295081968
val Loss: 0.192565394480244 Acc: 0.9411764705882353

Epoch 9/14
----------
train Loss: 0.17762072372143387 Acc: 0.9385245901639344
val Loss: 0.19549715163466197 Acc: 0.9411764705882353

Epoch 10/14
----------
train Loss: 0.19314993575948183 Acc: 0.9180327868852459
val Loss: 0.2000840900380627 Acc: 0.934640522875817

Epoch 11/14
----------
train Loss: 0.21551114418467537 Acc: 0.9057377049180327
val Loss: 0.18960770005299374 Acc: 0.934640522875817

Epoch 12/14
----------
train Loss: 0.1847396502729322 Acc: 0.9426229508196722
val Loss: 0.1871058808432685 Acc: 0.9411764705882353

Epoch 13/14
----------
train Loss: 0.17342406132670699 Acc: 0.9508196721311475
val Loss: 0.20636656588199093 Acc: 0.9215686274509803

Epoch 14/14
----------
train Loss: 0.16013679030488748 Acc: 0.9508196721311475
val Loss: 0.18491691759988374 Acc: 0.9411764705882353

Training compete in 0.0m 14.700076580047607s
Best val Acc: 0.954248366013072

In [130]:

1
2
3
4
5
6
# Initialize the non-pretrained version of the model used for this run
scratch_model,_ = initialize_model(model_name, num_classes, feature_extract=False, use_pretrained=False)
scratch_model = scratch_model.to(device)
scratch_optimizer = optim.SGD(scratch_model.parameters(), lr=0.001, momentum=0.9)
scratch_criterion = nn.CrossEntropyLoss()
_,scratch_hist = train_model(scratch_model, dataloaders_dict, scratch_criterion, scratch_optimizer, num_epochs=num_epochs)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
Epoch 0/14
----------
train Loss: 0.7185551504619786 Acc: 0.4426229508196721
val Loss: 0.6956208067781785 Acc: 0.45751633986928103

Epoch 1/14
----------
train Loss: 0.6852761008700387 Acc: 0.5778688524590164
val Loss: 0.6626271987273022 Acc: 0.6601307189542484

Epoch 2/14
----------
train Loss: 0.6603062289660094 Acc: 0.5942622950819673
val Loss: 0.6489538297154545 Acc: 0.5816993464052288

Epoch 3/14
----------
train Loss: 0.6203305486772881 Acc: 0.639344262295082
val Loss: 0.6013184107986151 Acc: 0.673202614379085

Epoch 4/14
----------
train Loss: 0.5989709232674271 Acc: 0.6680327868852459
val Loss: 0.5929347966231552 Acc: 0.6993464052287581

Epoch 5/14
----------
train Loss: 0.5821619336722327 Acc: 0.6557377049180327
val Loss: 0.5804777059679717 Acc: 0.6928104575163399

Epoch 6/14
----------
train Loss: 0.6114685896967278 Acc: 0.6270491803278688
val Loss: 0.5674225290616354 Acc: 0.7189542483660131

Epoch 7/14
----------
train Loss: 0.5681056575696977 Acc: 0.6680327868852459
val Loss: 0.5602688086188696 Acc: 0.7189542483660131

Epoch 8/14
----------
train Loss: 0.5701596453541615 Acc: 0.7090163934426229
val Loss: 0.5554519264526616 Acc: 0.7450980392156863

Epoch 9/14
----------
train Loss: 0.5476810380083615 Acc: 0.7254098360655737
val Loss: 0.5805927063125411 Acc: 0.7189542483660131

Epoch 10/14
----------
train Loss: 0.5508710468401674 Acc: 0.6926229508196722
val Loss: 0.5859468777974447 Acc: 0.7058823529411765

Epoch 11/14
----------
train Loss: 0.5344281519045595 Acc: 0.7172131147540983
val Loss: 0.5640550851821899 Acc: 0.7058823529411765

Epoch 12/14
----------
train Loss: 0.5125471890949812 Acc: 0.7295081967213115
val Loss: 0.5665123891207128 Acc: 0.7058823529411765

Epoch 13/14
----------
train Loss: 0.496260079204059 Acc: 0.7254098360655737
val Loss: 0.5820710787586137 Acc: 0.7058823529411765

Epoch 14/14
----------
train Loss: 0.49067981907578767 Acc: 0.7704918032786885
val Loss: 0.5722863315756804 Acc: 0.7058823529411765

Training compete in 0.0m 18.418847799301147s
Best val Acc: 0.7450980392156863

In [134]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Plot the training curves of validation accuracy vs. number 
# of training epochs for the transfer learning method and
# the model trained from scratch
# ohist = []
# shist = []

# ohist = [h.cpu().numpy() for h in ohist]
# shist = [h.cpu().numpy() for h in scratch_hist]

plt.title("Validation Accuracy vs. Number of Training Epochs")
plt.xlabel("Training Epochs")
plt.ylabel("Validation Accuracy")
plt.plot(range(1,num_epochs+1),ohist,label="Pretrained")
plt.plot(range(1,num_epochs+1),scratch_hist,label="Scratch")
plt.ylim((0,1.))
plt.xticks(np.arange(1, num_epochs+1, 1.0))
plt.legend()
plt.show()

丘吉尔的人物传记char级别的文本生成代码注释

读取数据

举个小小的例子,来看看LSTM是怎么玩的

我们这里用温斯顿丘吉尔的人物传记作为我们的学习语料。

第一步,一样,先导入各种库

关于LSTM的详细原理先看博客:如何从RNN起步,一步一步通俗理解LSTM

In [68]:

1
2
3
4
5
6
7
import numpy
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Dropout
from keras.layers import LSTM
from keras.callbacks import ModelCheckpoint
from keras.utils import np_utils

In [69]:

1
2
3
4
raw_text = open('./input/Winston_Churchil.txt').read()
# .read() 读入整个文件为一个字符串
raw_text = raw_text.lower() # 小写
print(raw_text[:100]) #打印前100个字符
1
2
3
project gutenberg’s real soldiers of fortune, by richard harding davis

this ebook is for the use o

既然我们是以每个字母为层级,字母总共才26个,所以我们可以很方便的用One-Hot来编码出所有的字母(当然,可能还有些标点符号和其他noise)

In [70]:

1
2
3
4
5
6
7
8
chars = sorted(list(set(raw_text))) 
# 这里去重不是单词去重,是字母去重,去重后只有英文字母和标点符号等字符
print(len(raw_text))
print(len(chars))
char_to_int = dict((c, i) for i, c in enumerate(chars))
# 去重后的字符排序好后,xi
int_to_char = dict((i, c) for i, c in enumerate(chars))
print(char_to_int)
1
2
3
276830
61
{'\n': 0, ' ': 1, '!': 2, '#': 3, '$': 4, '%': 5, '(': 6, ')': 7, '*': 8, ',': 9, '-': 10, '.': 11, '/': 12, '0': 13, '1': 14, '2': 15, '3': 16, '4': 17, '5': 18, '6': 19, '7': 20, '8': 21, '9': 22, ':': 23, ';': 24, '?': 25, '@': 26, '[': 27, ']': 28, '_': 29, 'a': 30, 'b': 31, 'c': 32, 'd': 33, 'e': 34, 'f': 35, 'g': 36, 'h': 37, 'i': 38, 'j': 39, 'k': 40, 'l': 41, 'm': 42, 'n': 43, 'o': 44, 'p': 45, 'q': 46, 'r': 47, 's': 48, 't': 49, 'u': 50, 'v': 51, 'w': 52, 'x': 53, 'y': 54, 'z': 55, '‘': 56, '’': 57, '“': 58, '”': 59, '\ufeff': 60}

构造训练集

我们这里简单的文本预测就是,给了前置的字母以后,下一个字母是谁?
我们需要把我们的raw text变成可以用来训练的x,y:

x 是前置字母们 y 是后一个字母

In [71]:

1
2
3
4
5
6
7
8
9
10
11
seq_length = 100 
# 输入的字符的长度,一个字符对应一个神经元,总共100个神经元
# 输入是100个字符,输出是预测的一个字符

x = []
y = []
for i in range(0, len(raw_text) - seq_length): # 每次循环都滑动一个字符距离
given = raw_text[i:i + seq_length] # 从零先取前100个字符作为输入
predict = raw_text[i + seq_length] # y是后一个字符
x.append([char_to_int[char] for char in given]) # 把字符转化为向量
y.append(char_to_int[predict]) # # 把字符转化为向量

In [72]:

1
2
3
4
print(x[:3]) 
print(y[:3])
print(set(y)) # 这里注意下,'\ufeff': 60这个字符是文本的首字符,只有这么一个,y是取不到的,所以y值的可能只有60种
print(len(set(y)))
1
2
3
4
[[60, 45, 47, 44, 39, 34, 32, 49, 1, 36, 50, 49, 34, 43, 31, 34, 47, 36, 57, 48, 1, 47, 34, 30, 41, 1, 48, 44, 41, 33, 38, 34, 47, 48, 1, 44, 35, 1, 35, 44, 47, 49, 50, 43, 34, 9, 1, 31, 54, 1, 47, 38, 32, 37, 30, 47, 33, 1, 37, 30, 47, 33, 38, 43, 36, 1, 33, 30, 51, 38, 48, 0, 0, 49, 37, 38, 48, 1, 34, 31, 44, 44, 40, 1, 38, 48, 1, 35, 44, 47, 1, 49, 37, 34, 1, 50, 48, 34, 1, 44], [45, 47, 44, 39, 34, 32, 49, 1, 36, 50, 49, 34, 43, 31, 34, 47, 36, 57, 48, 1, 47, 34, 30, 41, 1, 48, 44, 41, 33, 38, 34, 47, 48, 1, 44, 35, 1, 35, 44, 47, 49, 50, 43, 34, 9, 1, 31, 54, 1, 47, 38, 32, 37, 30, 47, 33, 1, 37, 30, 47, 33, 38, 43, 36, 1, 33, 30, 51, 38, 48, 0, 0, 49, 37, 38, 48, 1, 34, 31, 44, 44, 40, 1, 38, 48, 1, 35, 44, 47, 1, 49, 37, 34, 1, 50, 48, 34, 1, 44, 35], [47, 44, 39, 34, 32, 49, 1, 36, 50, 49, 34, 43, 31, 34, 47, 36, 57, 48, 1, 47, 34, 30, 41, 1, 48, 44, 41, 33, 38, 34, 47, 48, 1, 44, 35, 1, 35, 44, 47, 49, 50, 43, 34, 9, 1, 31, 54, 1, 47, 38, 32, 37, 30, 47, 33, 1, 37, 30, 47, 33, 38, 43, 36, 1, 33, 30, 51, 38, 48, 0, 0, 49, 37, 38, 48, 1, 34, 31, 44, 44, 40, 1, 38, 48, 1, 35, 44, 47, 1, 49, 37, 34, 1, 50, 48, 34, 1, 44, 35, 1]]
[35, 1, 30]
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59}
60

此刻,楼上这些表达方式,类似就是一个词袋,或者说 index。

接下来我们做两件事:

1.我们已经有了一个input的数字表达(index),我们要把它变成LSTM需要的数组格式: [样本数,时间步伐,特征]

2.对于output,我们在Word2Vec里学过,用one-hot做output的预测可以给我们更好的效果,相对于直接预测一个准确的y数值的话。

In [73]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
n_patterns = len(x) # 样本数量
n_vocab = len(chars) #

# 把x变成LSTM需要的样子
x = numpy.reshape(x, (n_patterns, seq_length, 1))
# n_patterns 样本的数量
# seq_length 每个样本每次训练100个字符
# 1 代表我们的一个字符的维度是1维,这里1维是特殊情况,如果我们的输入不是一个字符,
#是一个单词的话,单词是可以embedding成100维向量,那这里就是100.

x = x / float(n_vocab) # 简单normal到0-1之间,归一化,防止梯度爆炸
y = np_utils.to_categorical(y)
# y值总共取值61种,直接做个onehot编码
# 对类别进行onehot编码一个很重要的原因在于计算loss时的问题。loss一般用距离来表示,
# 如果用1~5来表示,那么1和2的距离是1,而1和5的距离是4,但是按道理1和2、1和5的距离应该一样。
# 如果用one hot编码表示,那么1和2的距离跟1和5的距离时一样的。

print(x[10][:10]) # 第10个样本的前10个字符向量
print(y[10])
print(y.shape) #
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[[0.81967213]
[0.80327869]
[0.55737705]
[0.70491803]
[0.50819672]
[0.55737705]
[0.7704918 ]
[0.59016393]
[0.93442623]
[0.78688525]]
[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. 1. 0. 0. 0. 0.
0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
(276730, 60)

构建和训练模型

In [74]:

1
2
3
4
5
6
7
8
9
10
model = Sequential()
# Sequential():序贯模型是多个网络层的线性堆叠,可以通过.add()一个一个添加层
model.add(LSTM(256, input_shape=(x.shape[1], x.shape[2])))
# 256是LSTM的隐藏层的维度。
# input_shape,不需要考虑样本数量,对模型本身结构没有意义
model.add(Dropout(0.2)) # 去掉20%的神经元
model.add(Dense(y.shape[1], activation='softmax'))
# y.shape[1] = 60,'softmax'转化为概率,概率和为1
model.compile(loss='categorical_crossentropy', optimizer='adam')
# 多分类交叉熵损失,adam优化器

In [75]:

1
2
model.fit(x, y, nb_epoch=1, batch_size=1024)
# 这里本地跑的太慢了,nb_epoch设置为1了
1
2
/Users/yyg/anaconda3/lib/python3.6/site-packages/ipykernel_launcher.py:1: UserWarning: The `nb_epoch` argument in `fit` has been renamed `epochs`.
"""Entry point for launching an IPython kernel.
1
2
Epoch 1/1
276730/276730 [==============================] - 1152s 4ms/step - loss: 3.0591

Out[75]:

1
<keras.callbacks.History at 0xb27cb07f0>

预测模型

In [95]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def predict_next(input_array):
x = numpy.reshape(input_array, (1, seq_length, 1))
# 同上,只有一个样本,reshape成lstm需要的维度。
x = x / float(n_vocab) # 归一化
y = model.predict(x) # y是60维的向量
return y

def string_to_index(raw_input):
res = []
for c in raw_input[(len(raw_input)-seq_length):]:
# 这步一个问题是len(raw_input)一定要大于seq_length:100,不然会报错
res.append(char_to_int[c]) # 得到输入句子中后100个字符的向量表示
return res

def y_to_char(y):
largest_index = y.argmax() # 取出概率最大的值对应的索引
c = int_to_char[largest_index] # 根据索引得到对应的字符
return c

In [96]:

1
2
3
4
5
6
7
8
9
def generate_article(init, rounds=200): 
# rounds=200 代表预测生成200个新的字符
# init是输入的字符串
in_string = init.lower() # 跟上面一样的预处理步骤
for i in range(rounds): # 每次模型预测一个字符
n = y_to_char(predict_next(string_to_index(in_string)))
# n是预测的新的字符
in_string += n # 把新的字符拼接,重新作为新的输入
return in_string

In [97]:

1
2
3
init = 'His object in coming to New York was to engage officers for that service. He came at an opportune moment'
article = generate_article(init)
print(article) # 这里上面只是迭代了一次,导致后面所有的预测都是空格
1
his object in coming to new york was to engage officers for that service. he came at an opportune moment

In [ ]:

1
 
|