内容简介:本文为 AI 研习社编译的技术博客,原标题 :Solving NLP task using Sequence2Sequence model: from Zero to Hero
本文为 AI 研习社编译的技术博客,原标题 :
Solving NLP task using Sequence2Sequence model: from Zero to Hero
作者 | Dima Shulga
翻译 | 邓普斯•杰弗、Zedom0 编辑 | 王立鱼
原文链接:
https://towardsdatascience.com/solving-nlp-task-using-sequence2sequence-model-from-zero-to-hero-c193c1bd03d1
注:本文的相关链接请访问文末【阅读原文】
今天我想要解决一个非常流行的NLP任务,它叫做命名实体识别(NER)。简 单来说,NER是从单词序列(一个句子)中抽取命名实体的任务。例如,给出下列句子:
"Jim在2006年买了Acme公司的300股"
我们会说:"Jim"是一个人,"Acme"是一个组织,"2006"是时间。
为了完成命名实体识别任务,我会使用公开的Kaggle数据集(dataset),跳过所有的数据处理代码,专注于实际的问题和它的解决方案。你可以在这个notebook中看到完整代码。 在这个数据集中有很多的实体类型,如个人(PER),组织(ORG)等等,每个实体类型都有两种标签:"B-SOMETAG" 和 "I-SOMETAG". B代表实体名的开始,I代表这个实体的延续。如果我们有一个实体:世界卫生组织",对应的标签就是: [B-ORG, I-ORG, I-ORG]
这有个从数据集中获取的样例:
import pandas as pd
ner_df = pd.read_csv('ner_dataset.csv')
ner_df.head(30)
由此我们得到了一些单词序列(一个句子),然后我们想要预测每个词的类别。这跟分类、回归这样的平常的机器学习任务不同,我们得到了一个序列,输出的也将是同样长度的序列。
有很多方法来解决命名实体识别问题,以下是我的策略:
把这个任务视作给每个句子每个词的分类任务,以此构建一个非常简单的模型,并把这个模型作为基准。
用Keras构建一个 序列到序列(Seq2Seq)的模型。
找到如何正确衡量与比较结果的方法。
在Seq2Seq模型中使用Glove预训练的词嵌入(embedding)。
你可以随意选择任何部分。
词袋和多分类
如我先前提及的那样,我们的输出是一个类别序列。首先,我想要尝试一个朴素的方法:一个简单的多分类模型。我想要把每个句子中的每个词看作是一个单独的实例,然后对于每个词来预测它的类别,类别可能是O,B-ORG, I-ORG, B-PER 等等。 这当然不是对这个问题建模的最佳方法,但我想这样做有两个原因:首先,我想要创建一个尽可能简单的基准模型。其次,我想要表明序列对序列模型在我们处理序列时表现要优秀得多。 很多时候,当我们想要对现实生活中的问题进行建模的时候,我们通常不清楚我们到底要解决什么样的问题。有时我们尝试把这些问题建模成简单的分类任务,但实际上用序列模型或许会更好。
如我所说,我把这个方法作为基准,并把问题尽可能简化,所以对每个词(实例),我的特征会是词向量(词袋)以及同个句子中的所有其它词。我的目标变量是17个类标签中的一个。
def sentence_to_instances(words, tags, bow, count_vectorizer):
X = []
y = []
for w, t in zip(words, tags):
v = count_vectorizer.transform([w])[0]
v = scipy.sparse.hstack([v, bow])
X.append(v)
y.append(t)
return scipy.sparse.vstack(X), y
因此给定如下句子:
“The World Health Organization says 227 people have died from bird flu”
我们得到了12个实例:
the O
world B-org
health I-org
organization I-org
says O
227 O
people O
have O
died O
from O
bird O
flu O
现在我们的任务是,给定一个句子中的一个词,预测它的类别
我们的数据集中有47958个句子,我们把它们划分到训练集和测试集中:
train_size = int(len(sentences_words) * 0.8)
train_sentences_words = sentences_words[:train_size]
train_sentences_tags = sentences_tags[:train_size]
test_sentences_words = sentences_words[train_size:]
test_sentences_tags = sentences_tags[train_size:]
# ============== Output ==============================
Train: 38366
Test: 9592
我们用上面的方法把所有的句子转换成很多个词的实例。在训练集中我们有839214个单词实例。
train_X, train_y = sentences_to_instances(train_sentences_words,
train_sentences_tags,
count_vectorizer)
print 'Train X shape:', train_X.shape
print 'Train Y shape:', train_y.shape
# ============== Output ==============================
Train X shape: (839214, 50892)
Train Y shape: (839214,)
我们的X有50892维,其中包括: 当前词的独热(one hot)向量,同个句子中所有其它词的词袋(Bag of words)向量。
我们用梯度上升分类器作为预测器:
clf = GradientBoostingClassifier().fit(train_X, train_y)
predicted = clf.predict(test_X)
print classification_report(test_y, predicted)
由此得到:
precision recall f1-score support
B-art 0.57 0.05 0.09 82
B-eve 0.68 0.28 0.40 46
B-geo 0.91 0.40 0.56 7553
B-gpe 0.96 0.84 0.90 3242
B-nat 0.52 0.27 0.36 48
B-org 0.93 0.31 0.46 4082
B-per 0.80 0.52 0.63 3321
B-tim 0.91 0.66 0.76 4107
I-art 0.09 0.02 0.04 43
I-eve 0.33 0.02 0.04 44
I-geo 0.82 0.55 0.66 1408
I-gpe 0.86 0.62 0.72 40
I-nat 0.20 0.08 0.12 12
I-org 0.88 0.24 0.38 3470
I-per 0.93 0.25 0.40 3332
I-tim 0.67 0.15 0.25 1308
O 0.91 1.00 0.95 177215
avg / total 0.91 0.91 0.89 209353
它表现地好吗?难说,但起码看起来不差。 我们可能想到好几种改进模型的方法,但这不是本文的目的,如我所说,我想要让它成为一个非常简单的基准。
但我们现在面临一个问题:不能正确地衡量我们地模型。我们得到了每个词的精确率\召回率,但它没有告诉我们关于真实实体的任何东西,举个简单的例子,还是那个句子:
“The World Health Organization says 227 people have died from bird flu”
我们有3个类是ORG,如果只正确地预测出其中两个,会得到66%的正确率,但实际上我们并没有把"World Health Organization" 这个实体正确抽取出来,所以实际的正确率应该是0!
之后我会谈到更好的衡量命名实体识别的方法,但首先,构建我们的 "序列到序列"(Seq2Seq)模型吧。
序列到序列模型
前面的方法的一个主要缺点在于我们丢失了词之间的依赖信息。给定一个句子中的一个词,如果我们知道这个词的左边(或右边)的词是一个实体,那会有益于我们进行预测。如果我们为每个词构建实例的话,就很难做到这点,我们在预测的时候也无法获取这个信息。这是我们把整个句子作为一个样本实例的原因。
我们能够用很多不同的模型做到序列预测,如隐马尔科夫模型(HMM)、条件随机场(CRF)或许做的不错,但在这我想要用Keras实现一个循环神经网络(RNN)。
为了使用Keras,我们需要把我们的句子转换成数字序列,每个数字代表一个词,而后我们需要让所有数字序列等长,对此我们可以用Keras里的util函数来实现。
首先,我们构建一个分词器(Tokenizer)来帮我们把词转换成数字,值得注意的是,我们只能在训练集中构建分词器。
words_tokenizer = Tokenizer(num_words=VOCAB_SIZE,
filters=[],
oov_token='__UNKNOWN__')
words_tokenizer.fit_on_texts(map(lambda s: ' '.join(s),
train_sentences_words))
word_index = words_tokenizer.word_index
word_index['__PADDING__'] = 0
index_word = {i:w for w, i in word_index.iteritems()}
# ============== Output ==============================
print 'Unique tokens:', len(word_index)
然后,我们可以用分词器来创建数字序列并把它们填充成等长的序列:
train_sequences = words_tokenizer.texts_to_sequences(map(lambda s: ' '.join(s), train_sentences_words))
test_sequences = words_tokenizer.texts_to_sequences(map(lambda s: ' '.join(s), test_sentences_words))
train_sequences_padded = pad_sequences(train_sequences, maxlen=MAX_LEN)
test_sequences_padded = pad_sequences(test_sequences, maxlen=MAX_LEN)
print train_sequences_padded.shape, test_sequences_padded.shape
# ============== Output ==============================
(38366, 75) (9592, 75)
我们可以看到训练集有38366个序列,测试集中有9592个序列,每个序列有75个词。
对标签也进行相同的操作,我就跳过这部分的代码了,你可以在原文里看到这部分的代码。
print train_tags_padded.shape, test_tags_padded.shape
# ============== Output ==============================
(38366, 75, 1) (9592, 75, 1)
可以看到,训练集中有38366个序列,测试集中有9592个序列,每个序列有17个标签。
现在我们准备来构建模型了,我们将使用已被证明在这类任务中十分有效的双向长短时记忆(LSTM)层:
input = Input(shape=(75,), dtype='int32')
emb = Embedding(V_SIZE, 300, max_len=75)(input)
x = Bidirectional(LSTM(64, return_sequences=True))(emb)
preds = Dense(len(tag_index), activation='softmax')(x)
model = Model(sequence_input, preds)
model.compile(loss='sparse_categorical_crossentropy',
optimizer='adam',
metrics=['sparse_categorical_accuracy'])
来看一下上面的代码。
我们的第一层是 Input, 它接受维度是 (75,)的向量,这跟X变量匹配(我们训练集和测试集的序列长度为75)。
然后就是Embedding层,这个层会获取每个词并把它们转换成300维的稠密向量。我们可以把它看作是一个巨大的查询表(词典),词的id是键(key),而实际的向量是值(value)。这个查询表是可训练的,在模型每轮训练中,我们将会更新这些向量以此来匹配输入。
在经过Embedding层之后,我们的输入从长为75的向量变为维度为(75,300)的矩阵,75个词现在每个都有300维的向量。
一旦我们有了这个,我们就可以用双向LSTM层来让每个词看到它前后方向的词,并返回在之后有助于我们分类这个词的状态。默认来说,LSTM层会返回一个向量(最后一个),但在这个例子中,我们想要每个词一个向量,所以我们设置 return_sequences=True。
双向LSTM看起来如下图:
该层的输出是一个(75, 128)维的矩阵,75个词,两个方向每个64个数字。
最终我们用Time Distributed Dense层(当设置return_sequences=True时它由Dense 变为 Time Distributed Dense)
它将LSTM层的(75,128)维输出作为输入并返回需要的(75,18)维矩阵,75个词,每个词分别有17个标签的概率,最后一个为__PADDING__标签。
调用model.summary()方法就可以非常清晰地看到发生了什么:
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_1 (InputLayer) (None, 75) 0
_________________________________________________________________
embedding_1 (Embedding) (None, 75, 300) 8646600
_________________________________________________________________
bidirectional_1 (Bidirection (None, 75, 128) 186880
_________________________________________________________________
dense_2 (Dense) (None, 75, 18) 627
=================================================================
Total params: 8,838,235
Trainable params: 8,838,235
Non-trainable params: 0
_________________________________________________________________
你可以看到所有的层以及它们的输入输出维度。此外,我们还可以看到模型的参数数目。你可能会注意到embedding层有最多的参数,原因在于我们有很多词,每个词都需要学习300个数字。后文中,我会用预训练的embedding来改进我们的模型。
我们来训练这个模型吧:
model.fit(train_sequences_padded, train_tags_padded,
batch_size=32,
epochs=10,
validation_data=(test_sequences_padded, test_tags_padded))
# ============== Output ==============================
Train on 38366 samples, validate on 9592 samples
Epoch 1/10
38366/38366 [==============================] - 274s 7ms/step - loss: 0.1307 - sparse_categorical_accuracy: 0.9701 - val_loss: 0.0465 - val_sparse_categorical_accuracy: 0.9869
Epoch 2/10
38366/38366 [==============================] - 276s 7ms/step - loss: 0.0365 - sparse_categorical_accuracy: 0.9892 - val_loss: 0.0438 - val_sparse_categorical_accuracy: 0.9879
Epoch 3/10
38366/38366 [==============================] - 264s 7ms/step - loss: 0.0280 - sparse_categorical_accuracy: 0.9914 - val_loss: 0.0470 - val_sparse_categorical_accuracy: 0.9880
Epoch 4/10
38366/38366 [==============================] - 261s 7ms/step - loss: 0.0229 - sparse_categorical_accuracy: 0.9928 - val_loss: 0.0480 - val_sparse_categorical_accuracy: 0.9878
Epoch 5/10
38366/38366 [==============================] - 263s 7ms/step - loss: 0.0189 - sparse_categorical_accuracy: 0.9939 - val_loss: 0.0531 - val_sparse_categorical_accuracy: 0.9878
Epoch 6/10
38366/38366 [==============================] - 294s 8ms/step - loss: 0.0156 - sparse_categorical_accuracy: 0.9949 - val_loss: 0.0625 - val_sparse_categorical_accuracy: 0.9874
Epoch 7/10
38366/38366 [==============================] - 318s 8ms/step - loss: 0.0129 - sparse_categorical_accuracy: 0.9958 - val_loss: 0.0668 - val_sparse_categorical_accuracy: 0.9872
Epoch 8/10
38366/38366 [==============================] - 275s 7ms/step - loss: 0.0107 - sparse_categorical_accuracy: 0.9965 - val_loss: 0.0685 - val_sparse_categorical_accuracy: 0.9869
Epoch 9/10
38366/38366 [==============================] - 270s 7ms/step - loss: 0.0089 - sparse_categorical_accuracy: 0.9971 - val_loss: 0.0757 - val_sparse_categorical_accuracy: 0.9870
Epoch 10/10
38366/38366 [==============================] - 266s 7ms/step - loss: 0.0076 - sparse_categorical_accuracy: 0.9975 - val_loss: 0.0801 - val_sparse_categorical_accuracy: 0.9867
我们在测试集上的准确率是98.6%。鉴于我们大多数的标签都是0(其它),因此这个准确率并没有告诉我们很多信息。我们想要看到先前那样的每个类的精确率和召回率,但如同我在前面小节提到的那样,这不是衡量我们的模型最好的方式。我们想要的是一种,能够知道我们可以正确预测多少不同类型实体的方法。
序列到序列模型的评估
在处理序列时,我们的标签/实体也可能是序列。如前所述,如果把“World Health Organisation”作为真正的实体,那么预测“World Organisation”或“World Health”可能会使我们在词汇层面上的准确率达到66%,但两者都是错误的预测。我们想要将每个句子中的所有实体包装起来,并将它们与预测的实体进行比较。
我们可以使用优秀的Seqeval库来实现这个点。对于每个句子,它查找所有不同的标记并构造实体。通过同时对真标记和预测标记进行操作,我们可以比较真实的实体值,而不仅仅是单词。在这种情况下,没有“b-”或“i-”标记,我们比较实体的实际类型,而不是单词类。
使用我们的预测值,这是一个概率矩阵,我们想为每个句子构建一个标签序列,其原始长度(而不是我们所做的75),这样我们就可以将它们与真实值进行比较。我们将为我们的LSTM模型和我们的词袋模型执行此操作:
lstm_predicted = model.predict(test_sequences_padded)
lstm_predicted_tags = []
bow_predicted_tags = []
for s, s_pred in zip(test_sentences_words, lstm_predicted):
tags = np.argmax(s_pred, axis=1)
tags = map(index_tag_wo_padding.get,tags)[-len(s):]
lstm_predicted_tags.append(tags)
bow_vector, _ = sentences_to_instances([s],
[['x']*len(s)],
count_vectorizer)
bow_predicted = clf.predict(bow_vector)[0]
bow_predicted_tags.append(bow_predicted)
现在我们准备用seqeval 库来对模型进行评估:
from seqeval.metrics import classification_report, f1_score
print 'LSTM'
print '='*15
print classification_report(test_sentences_tags,
lstm_predicted_tags)
print 'BOW'
print '='*15
print classification_report(test_sentences_tags, bow_predicted_tags)
得到:
LSTM
===============
precision recall f1-score support
art 0.11 0.10 0.10 82
gpe 0.94 0.96 0.95 3242
eve 0.21 0.33 0.26 46
per 0.66 0.58 0.62 3321
tim 0.84 0.83 0.84 4107
nat 0.00 0.00 0.00 48
org 0.58 0.55 0.57 4082
geo 0.83 0.83 0.83 7553
avg / total 0.77 0.75 0.76 22481
BOW
===============
precision recall f1-score support
art 0.00 0.00 0.00 82
gpe 0.01 0.00 0.00 3242
eve 0.00 0.00 0.00 46
per 0.00 0.00 0.00 3321
tim 0.00 0.00 0.00 4107
nat 0.00 0.00 0.00 48
org 0.01 0.00 0.00 4082
geo 0.03 0.00 0.00 7553
avg / total 0.01 0.00 0.00 22481
这里很不同。你可以看到bow模型几乎不能正确预测任何结果,而lstm模型做得更好。
当然,我们可以在BOW模型上做更多的工作,取得更好的效果,但整体上是清楚的,在这种情况下,序列到序列模型效果更好。
预训练的词嵌入(词表达)
正如之前看到的,我们的大多数模型参数是用于嵌入层的。因为有大量单词,而且训练数据有限,所以训练这一层非常困难。使用预先训练过的嵌入层是非常常见的情况。目前大多数嵌入模型都使用所谓的“分布假设”,即在相似的上下文中,单词具有相似的含义。通过构建一个模型来预测给定上下文的单词(或相反的方式),它们可以生成对单词含义有良好表示的单词向量。虽然它与我们的任务没有直接关系,但是使用这些嵌入可能有助于我们的模型更好地表示其目标的单词。
还有其他方法来构建单词嵌入,从简单的共现矩阵到更复杂的语言模型。在这篇文章中,我尝试使用图像构建单词嵌入。
这里我们将使用流行的Glove嵌入。word2vec或任何其他实现也可以实现。
我们需要下载glove,加载单词向量并创建嵌入矩阵。我们将使用此矩阵作为嵌入层的不可训练权重:
embeddings = {}
with open(os.path.join(GLOVE_DIR, 'glove.6B.300d.txt')) as f:
for line in f:
values = line.split()
word = values[0]
coefs = np.asarray(values[1:], dtype='float32')
embeddings[word] = coefs
num_words = min(VOCAB_SIZE, len(word_index) + 1)
embedding_matrix = np.zeros((num_words, 300))
for word, i in word_index.items():
if i >= VOCAB_SIZE:
continue
embedding_vector = embeddings.get(word)
if embedding_vector is not None:
embedding_matrix[i] = embedding_vector
现在我们的模型:
input = Input(shape=(75,), dtype='int32')
emb = Embedding(VOCAB_SIZE, 300,
embeddings_initializer=Constant(embedding_matrix),
input_length=MAX_LEN,
trainable=False)(input)
x = Bidirectional(LSTM(64, return_sequences=True))(emb)
preds = Dense(len(tag_index), activation='softmax')(x)
model = Model(sequence_input, preds)
model.compile(loss='sparse_categorical_crossentropy',
optimizer='adam',
metrics=['sparse_categorical_accuracy'])
model.summary()
# ============== Output ==============================
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_2 (InputLayer) (None, 75) 0
_________________________________________________________________
embedding_2 (Embedding) (None, 75, 300) 8646600
_________________________________________________________________
bidirectional_2 (Bidirection (None, 75, 128) 186880
_________________________________________________________________
dropout_2 (Dropout) (None, 75, 128) 0
_________________________________________________________________
dense_4 (Dense) (None, 75, 18) 627
=================================================================
Total params: 8,838,235
Trainable params: 191,635
Non-trainable params: 8,646,600
_________________________________________________________________
一切都和以前一样。唯一的区别是现在我们的嵌入层具有恒定的不可训练权重。您可以看到,总参数的数量没有改变,而可训练的参数数量要低得多。
让我们训练模型:
Train on 38366 samples, validate on 9592 samples
Epoch 1/10
38366/38366 [==============================] - 143s 4ms/step - loss: 0.1401 - sparse_categorical_accuracy: 0.9676 - val_loss: 0.0514 - val_sparse_categorical_accuracy: 0.9853
Epoch 2/10
38366/38366 [==============================] - 143s 4ms/step - loss: 0.0488 - sparse_categorical_accuracy: 0.9859 - val_loss: 0.0429 - val_sparse_categorical_accuracy: 0.9875
Epoch 3/10
38366/38366 [==============================] - 138s 4ms/step - loss: 0.0417 - sparse_categorical_accuracy: 0.9876 - val_loss: 0.0401 - val_sparse_categorical_accuracy: 0.9881
Epoch 4/10
38366/38366 [==============================] - 132s 3ms/step - loss: 0.0381 - sparse_categorical_accuracy: 0.9885 - val_loss: 0.0391 - val_sparse_categorical_accuracy: 0.9887
Epoch 5/10
38366/38366 [==============================] - 146s 4ms/step - loss: 0.0355 - sparse_categorical_accuracy: 0.9891 - val_loss: 0.0367 - val_sparse_categorical_accuracy: 0.9891
Epoch 6/10
38366/38366 [==============================] - 143s 4ms/step - loss: 0.0333 - sparse_categorical_accuracy: 0.9896 - val_loss: 0.0373 - val_sparse_categorical_accuracy: 0.9891
Epoch 7/10
38366/38366 [==============================] - 145s 4ms/step - loss: 0.0318 - sparse_categorical_accuracy: 0.9900 - val_loss: 0.0355 - val_sparse_categorical_accuracy: 0.9894
Epoch 8/10
38366/38366 [==============================] - 142s 4ms/step - loss: 0.0303 - sparse_categorical_accuracy: 0.9904 - val_loss: 0.0352 - val_sparse_categorical_accuracy: 0.9895
Epoch 9/10
38366/38366 [==============================] - 138s 4ms/step - loss: 0.0289 - sparse_categorical_accuracy: 0.9907 - val_loss: 0.0362 - val_sparse_categorical_accuracy: 0.9894
Epoch 10/10
38366/38366 [==============================] - 137s 4ms/step - loss: 0.0278 - sparse_categorical_accuracy: 0.9910 - val_loss: 0.0358 - val_sparse_categorical_accuracy: 0.9895
准确度变化不大,但正如我们之前看到的,准确度并不是正确的衡量标准。让我们以正确的方式评估它,并与以前的模型进行比较:
lstm_predicted_tags = []
for s, s_pred in zip(test_sentences_words, lstm_predicted):
tags = np.argmax(s_pred, axis=1)
tags = map(index_tag_wo_padding.get,tags)[-len(s):]
lstm_predicted_tags.append(tags)
print 'LSTM + Pretrained Embbeddings'
print '='*15
print classification_report(test_sentences_tags, lstm_predicted_tags)
# ============== Output ==============================
LSTM + Pretrained Embbeddings
===============
precision recall f1-score support
art 0.45 0.06 0.11 82
gpe 0.97 0.95 0.96 3242
eve 0.56 0.33 0.41 46
per 0.72 0.71 0.72 3321
tim 0.87 0.84 0.85 4107
nat 0.00 0.00 0.00 48
org 0.62 0.56 0.59 4082
geo 0.83 0.88 0.86 7553
avg / total 0.80 0.80 0.80 22481
非常好!我们的F1分数从76提高到80!
结论:
序列到序列模型对于许多任务来说都是非常强大的模型,比如命名实体识别(NER)、词性(POS)标注、解析等等。有许多技术和方法可以训练它们,但最重要的是知道何时使用它们,以及如何正确地模拟我们的问题。
想要继续查看该篇文章相关链接和参考文献?
点击底部 【阅读原文】 即可访问:
https://ai.yanxishe.com/page/TextTranslation/1312
点击 阅读原文 ,查看本文更多内容
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
离心力:互联网历史与数字化未来
[英] 乔尼·赖安(Johnny Ryan) / 段铁铮 / 译言·东西文库/电子工业出版社 / 2018-2-1 / 68.00元
★一部详实、严谨的互联网史著作; ★哈佛、斯坦福等高校学生必读书目; ★《互联网的未来》作者乔纳森·L. 齐特雷恩,《独立报》《爱尔兰时报》等知名作者和国外媒体联合推荐。 【内容简介】 虽然互联网从诞生至今,不过是五六十年,但我们已然有必要整理其丰富的历史。未来的数字世界不仅取决于我 们的设想,也取决于它的发展历程,以及互联网伟大先驱们的理想和信念。 本书作者乔尼· ......一起来看看 《离心力:互联网历史与数字化未来》 这本书的介绍吧!