一. 前言

自然语言处理(NLP)是机器学习,人工智能中的一个重要领域。文本表达是 NLP中的基础技术,文本分类则是 NLP 的重要应用。在 2016 年, Facebook Research 开源了名为 fasttext[1] 的文本表达和分类的计算库。 fasttext 是基于文章 [2], [3], [4] 所提出算法的实现,针对变形词汇表达,线性分类优化提供了优秀的解决方案。 本文试图梳理 FastText 在文本表达和文本分类方面的工作,并进行实践。

二. 词嵌入

1. 背景介绍

词表达是 NLP 处理中的关键技术。常见的方式如 one-hot,使用了高维稀疏向量表示词汇,这样的特征可以反映词出现的频率,却不能反映词之间的关系。与此对应,词嵌入技术将词汇的上下文关系嵌入一个低维空间,其中比较有代表性的方法如 word2vec [5], GloVe[6]。本段落将分析 word2vec 的原理。

2. word2vec

word2vec 将词的上下文关系嵌入到低维空间。更具体而言,word2vec 将词的上下文关系转换为分类关系,并以此同时训练词嵌入向量和 logistic regression 分类器。

1. 逻辑回归

logistic regression 是经典的线性分类模型。广义上而言,线性模型由三个部分组成, 1. 输入向量 2. 线性系数 3. 偏移(bias),而 bias 可以进一步表示成线性系数。所以,二分类的线性分类问题可以表示为输入 $x$ 和系数 $w$ 的内积结果 $w^Tx$,结果的正负决定了数据的类别。分类器参数通过最小化损失函数 $l(y, w^Tx)$来完成,不同的损失函数定义了不同的分类模型。

下面列举了svm, lr 的损失函数,其中 $y\in \{-1, 1\}$
logit : $\Sigma_{i=1}^N ln(1 + e^{-y_i w^Tx_i})$
svm : $\Sigma_{i=1}^N max(0, 1- y _iw^Tx_i) + \lambda||w||^2$

logistic regression 也广泛地应用在多分类问题中,通过 softmax 函数计算数据属于每个类别的概率完成分类。因为分类神经网络的输出层通常也设定为 softmax 函数,所以多分类 lr 也可以表示为浅层神经网络。

下面我们分析 word2vec 如何将词的上下文关系转化为分类任务。

2. skip-gram

我们将词的上下文定义为以词 $w_i$ 为中心,窗口为 $k$ 前后范围内的词 $C_i = {w_{i-k}, w_{i-k+1}, ..., w_{i-1}, w_{i+1}, ... w_{i+k}}$。

skip-gram 将词之间的关系变成了 $|V|$ 多分类问题,其中 $|V|$ 是词库大小。每个词有两个变量 $x_i, w_i$,前者为词嵌入向量,后者是线性分类器的系数,在相关文章中又称为上下文向量。

skip-gram 用中心词汇来预测其上下文,如下图:

 
skip-gram.png

3. cbow

Continuous bag of words(cbow) 是与 skip-gram 相对应的另一种将上下文转化为分类任务的方式。

 
image.png

上图显示 cbow 用上下文词向量的加和结果来预测其中心词汇,其它的方式还有拼接。需要注意的是拼接的方式会导致上下文向量 $c_i$ 维度增大 $2 k$ 倍数, $k$ 为上下文窗口。

4. 针对多分类的计算优化

word2vec 将上下文关系转化为多分类任务,进而训练逻辑回归模型,这里的类别数量是 $|V|$ 词库大小。通常的文本数据中,词库少则数万,多则百万,在训练中直接训练多分类逻辑回归并不现实。

word2vec [5] 中提供了两种针对大规模多分类问题的优化手段, negative sampling 和 hierarchical softmax。以 skip-gram 为例,中心词对上下文的词类是正面例子,对所有其它的词则是负面例子。在优化中,negative sampling 只更新少量负面类,从而减轻了计算量。hierarchical softmax 将词库表示成前缀树,从树根到叶子的路径可以表示为一系列二分类器,一次多分类计算的复杂度从 $|V|$ 降低到了树的高度。

3. 总结

词嵌入技术将词的上下文关系嵌入到低维空间。word2vec 将词的局部上下文转化为了多分类任务,从而训练逻辑回归模型,并将逻辑回归模型中的输入部分作为词嵌入输出。

三. FastText 子词嵌入

1. 背景介绍

词嵌入技术在 NLP 领域发挥了越来越大的作用,相较于 one-hot 特征,词嵌入具有维度低,体现词间关系等特点。但传统的词嵌入技术忽略了词的微变形特性,如英语中动词的第三人称,进行时或过去时变形。针对这个问题, [2] 提出使用子词来学习词的表达,每个词由内部的 n-gram 字母串组成。在中文处理中, [7] 提出了类似的算法。

2. FastText 的实现

FastText 的子词嵌入在 word2vec 的基础上,引入了子词这个因素,从而使得词的微变形关系也能映射到嵌入空间中。

1. 子词

在 fasttext 中,每个词被看做是 n-gram 字母串包。为了区分前后缀情况,"<", ">" 符号被加到了词的前后端。除了词的子串外,词本身也被包含进了 n-gram 字母串包。以 where 为例,$n=3$ 的情况下,其子串分别为
<wh, whe, her, ere, re>,以及其本身 <where>

注意,这里的 her 与单词 <her> 是不同的。

现在我们看看 fasttext 具体是怎么计算子串的。

void Dictionary::initNgrams() {
for (size_t i = 0; i < size_; i++) {
// 加入前后缀符号 <, >
std::string word = BOW + words_[i].word + EOW;
// 词本身作为特殊子串加入包中
words_[i].subwords.push_back(i);
// 计算并将词子串加入包中
computeNgrams(word, words_[i].subwords);
}
} // 计算词子串
void Dictionary::computeNgrams(const std::string& word,
std::vector<int32_t>& ngrams,
std::vector<std::string>& substrings) const {
for (size_t i = 0; i < word.size(); i++) {
std::string ngram;
if ((word[i] & 0xC0) == 0x80) continue;
for (size_t j = i, n = 1; j < word.size() && n <= args_->maxn; n++) {
ngram.push_back(word[j++]);
// 将 utf-8 编码数据部分推入 ngram
while (j < word.size() && (word[j] & 0xC0) == 0x80) {
ngram.push_back(word[j++]);
}
if (n >= args_->minn && !(n == 1 && (i == 0 || j == word.size()))) {
// fasttext 使用 open hash 进行 string->int 转换
int32_t h = hash(ngram) % args_->bucket;
ngrams.push_back(nwords_ + h);
substrings.push_back(ngram);
}
}
}
}

在 fasttext 中,minn 和 maxn 参数控制了子串的长度,其默认值分别为 3, 6。再以 where 为例,在默认子串参数情况下,其子串包内容为:

<wh, <whe, <wher, <where, whe, wher, where, where>, her, here, here>, ere, ere>, re> 以及自身 <where>

下面我们看 fasttext 是如何用子串来构造词嵌入的,这里注意每个子串也有自身的嵌入表示。

void Model::computeHidden(const std::vector<int32_t>& input, Vector& hidden) const {
assert(hidden.size() == hsz_);
hidden.zero();
for (auto it = input.cbegin(); it != input.cend(); ++it) {
if(quant_) {
// product quantization 优化
// 子串加和
hidden.addRow(*qwi_, *it);
} else {
// 子串加和
hidden.addRow(*wi_, *it);
}
}
// 平均
hidden.mul(1.0 / input.size());
}

代码非常简明直接,每个词是其子串嵌入表示的加和平均。

fasttext 的另一个优势,是学习未知词的表达。以 working 为例,如果这个词在学习文本中没有出现过,但文本包含了 work, 和 ing,那么 working 的词嵌入向量也能够合理地计算出来。

2. skip-gram

一个词的嵌入是其子串嵌入的加和平均。当这个嵌入表示计算出来后,后续步骤与 word2vec 的 skip-gram 相同。下面是 fasttext 的 skip-gram 实现代码:

void FastText::skipgram(Model& model, real lr,
const std::vector<int32_t>& line) {
std::uniform_int_distribution<> uniform(1, args_->ws);
for (int32_t w = 0; w < line.size(); w++) {
// 窗口大小
int32_t boundary = uniform(model.rng);
const std::vector<int32_t>& ngrams = dict_->getNgrams(line[w]);
for (int32_t c = -boundary; c <= boundary; c++) {
if (c != 0 && w + c >= 0 && w + c < line.size()) {
// 通过 ngrams 计算 line[w] 的表示,通过逻辑回归优化 ngrams 的嵌入表示
model.update(ngrams, line[w + c], lr);
}
}
}
}

我们来详细看看 Model::update 函数是怎么用逻辑回归来优化 ngrams 的嵌入表示

void Model::update(const std::vector<int32_t>& input, int32_t target, real lr) {
assert(target >= 0);
assert(target < osz_);
if (input.size() == 0) return;
// 子串加权平均得到词嵌入表示
computeHidden(input, hidden_); // 针对分类问题的 3 种处理
// 1. negative sampling 优化
if (args_->loss == loss_name::ns) {
loss_ += negativeSampling(target, lr);
}
// 2. hierarchical softmax 优化
else if (args_->loss == loss_name::hs) {
loss_ += hierarchicalSoftmax(target, lr);
}
// 3. 直接计算多分类逻辑回归
// 在文本分类模式下使用
else {
loss_ += softmax(target, lr);
}
nexamples_ += 1; // 逻辑回归产生了导数,在文本分类情况下导数向量需要除以子串数量
if (args_->model == model_name::sup) {
grad_.mul(1.0 / input.size());
}
// 将导数直接加到子串向量中
for (auto it = input.cbegin(); it != input.cend(); ++it) {
wi_->addRow(grad_, *it, 1.0);
}
}

在第二段,我们简单介绍了针对大量多分类的优化策略, negative sampling 和 hierarchical sampling。这里我们看看 fasttext 是怎样实现这些策略。

// 根据词频构构建采样表
void Model::initTableNegatives(const std::vector<int64_t>& counts) {
real z = 0.0;
for (size_t i = 0; i < counts.size(); i++) {
z += pow(counts[i], 0.5);
}
// 每个词类在采样表中数量与其词频开方成比例
for (size_t i = 0; i < counts.size(); i++) {
real c = pow(counts[i], 0.5);
for (size_t j = 0; j < c * NEGATIVE_TABLE_SIZE / z; j++) {
negatives.push_back(i);
}
}
std::shuffle(negatives.begin(), negatives.end(), rng);
} // 负采样
real Model::negativeSampling(int32_t target, real lr) {
real loss = 0.0;
grad_.zero();
// 负采样 args_->neg 个类别
for (int32_t n = 0; n <= args_->neg; n++) {
if (n == 0) {
// 将当前词作为正面例子对 target 类进行二分逻辑回归训练
loss += binaryLogistic(target, true, lr);
} else {
// 将当前词作为负面例子对负采样类进行二分逻辑回归训练
loss += binaryLogistic(getNegative(target), false, lr);
}
}
return loss;
} // 负采样一个类
int32_t Model::getNegative(int32_t target) {
int32_t negative;
// 轮询采样表
do {
negative = negatives[negpos];
negpos = (negpos + 1) % negatives.size();
} while (target == negative);
return negative;
}

下面我们看看 hierarchical softmax 的实现

// 根据词频构造前缀树,因为树的内部节点数量为 |V| - 1 所以可以用 |V| * 2 的数组存储树结构
void Model::buildTree(const std::vector<int64_t>& counts) {
tree.resize(2 * osz_ - 1);
for (int32_t i = 0; i < 2 * osz_ - 1; i++) {
tree[i].parent = -1;
tree[i].left = -1;
tree[i].right = -1;
tree[i].count = 1e15;
tree[i].binary = false;
}
for (int32_t i = 0; i < osz_; i++) {
tree[i].count = counts[i];
}
int32_t leaf = osz_ - 1;
int32_t node = osz_;
for (int32_t i = osz_; i < 2 * osz_ - 1; i++) {
int32_t mini[2];
for (int32_t j = 0; j < 2; j++) {
if (leaf >= 0 && tree[leaf].count < tree[node].count) {
mini[j] = leaf--;
} else {
mini[j] = node++;
}
}
tree[i].left = mini[0];
tree[i].right = mini[1];
tree[i].count = tree[mini[0]].count + tree[mini[1]].count;
tree[mini[0]].parent = i;
tree[mini[1]].parent = i;
tree[mini[1]].binary = true;
}
for (int32_t i = 0; i < osz_; i++) {
std::vector<int32_t> path;
std::vector<bool> code;
int32_t j = i;
while (tree[j].parent != -1) {
path.push_back(tree[j].parent - osz_);
code.push_back(tree[j].binary);
j = tree[j].parent;
}
paths.push_back(path);
codes.push_back(code);
}
} // 用 hierarchical softmax 进行多分类计算
real Model::hierarchicalSoftmax(int32_t target, real lr) {
real loss = 0.0;
grad_.zero();
// 词的 0,1 表示
const std::vector<bool>& binaryCode = codes[target];
// 词到树根的路径
const std::vector<int32_t>& pathToRoot = paths[target];
for (int32_t i = 0; i < pathToRoot.size(); i++) {
// 路径上的节点对应二分类, 0,1 编码决定分类
// 用二分逻辑回归进行训练
loss += binaryLogistic(pathToRoot[i], binaryCode[i], lr);
}
return loss;
}

最后我们看一下二分逻辑回归的实现。

// 用一个正/负面例子更新二分逻辑回归模型
real Model::binaryLogistic(int32_t target, bool label, real lr) {
real score = sigmoid(wo_->dotRow(hidden_, target));
real alpha = lr * (real(label) - score);
grad_.addRow(*wo_, target, alpha);
wo_->addRow(hidden_, target, alpha);
if (label) {
return -log(score);
} else {
return -log(1.0 - score);
}
}

3. cbow

第二段讲解了 cbow 的原理是用中心词的上下文来预测中心词, 这里我们看看 fasttext 是如何实现 cbow 的。

void FastText::cbow(Model& model, real lr,
const std::vector<int32_t>& line) {
std::vector<int32_t> bow;
std::uniform_int_distribution<> uniform(1, args_->ws);
for (int32_t w = 0; w < line.size(); w++) {
int32_t boundary = uniform(model.rng);
bow.clear();
for (int32_t c = -boundary; c <= boundary; c++) {
// 将上下文的子字符串加入包中
if (c != 0 && w + c >= 0 && w + c < line.size()) {
const std::vector<int32_t>& ngrams = dict_->getNgrams(line[w + c]);
bow.insert(bow.end(), ngrams.cbegin(), ngrams.cend());
}
}
// 用所有子字符串来预测中心词,从而更新参数
model.update(bow, line[w], lr);
}
}

fasttext 的处理非常简洁,将上下文的子串全部加和平均作为输入去预测中心词。

3. 总结

fasttext 利用子词改良了词嵌入的质量,在嵌入学习中考虑了词的内部结构。在具体实现中, fasttext 用子词向量的加和平均表示词向量, 提供了 skip-gram 和 cbow 模式训练词嵌入表达。

四. FastText 线性文本分类优化

1. 背景介绍

对于文本分类而言,线性分类器往往能够达到非常优秀,媲美深度模型的效果。通常,线性模型的参数数量与词库大小相关,导致模型规模巨大。 fasttext 针对线性分类器模型进行了诸多优化,在不显著损失分类器精度的情况下,减少了内存使用和计算时间。

2. fasttext 分类模型架构

fasttext 的词嵌入是通过分类学习完成的,所以词嵌入和文本分类模型可以用下图统一表示。

 
Model Architecture.png

其中 $x_1, ..., x_N$ 表示一个文本中的 ngram 向量,一个文本的表示是所有 ngram 的加和平均。这和前文中提到的 cbow 相似,cbow 用上下文的 ngram 去预测中心词,而此处用全部的 ngram 去预测指定类别。

与前文词嵌入模型一样, fasttext 模型在进行文本分类监督训练时,既学习词嵌入表达,也学习分类器线性系数。

3. 优化

1. 子空间量化

product quantization [8] 是一种保存数据间距离的压缩技术。PQ 用一个码本来近似数据,与传统的 keams 训练码本不同的是, PQ 将数据空间划分为 k 个子空间,并分别用 kmeans 学习子空间码本。数据的近似和重建均在子空间完成,最终拼接成结果。

在 fasttext 中,子空间码本大小为 256,可以用 1 byte 表示。子空间的数量在 [2, d/2] 间取值。

除了用 PQ 对数据进行量化压缩,fasttext 还提供了对分类系数的 PQ 量化选项。

PQ 的优化能够在不影响分类其表现的情况下,将分类模型压缩为原大小的 $\frac{1}{10}$。

2. 裁剪字典内容

fasttext 提供了一个诱导式裁剪字典的算法,保证裁剪后的字典内容覆盖了所有的文章。具体而言,fasttext 存有一个保留字典,并在线处理文章,如果新的文章没有被保留字典涵盖,则从该文章中提取一个 norm 最大的词和其子串加入字典中。

字典裁剪能够有效将模型的数量减少,甚至到原有的 $\frac{1}{100}$。

4. 总结

fasttext 利用 Product Quantization 对字典中的 词嵌入向量进行了压缩,并使用诱导式字典方法,构造涵盖全部文本的字典。两者结合,能够在不明显损害分类算法表现的情况下,将分类模型大小减小数百倍 。

五. 实践

本段我们对词嵌入进行实践。词嵌入的比较有两种方式,直接比较是验证词表达保存了人为标注的词间关系,间接比较则是通过使用嵌入表达向量进行进一步学习,比如情绪预测[5],通过模型的表现判断词嵌入的质量。

我们用中文维基数据 [9] 进行了训练,fasttext 的参数使用默认值, epoch 设置为50。此外 facebook 还提供了训练好的多国语言表达 [10].

以下我们分别用 nn(最近邻) 对中文维基训练结果进行实践:

// 乒乓球的近似词汇
Query word? 乒乓球
壁球 0.837808
曲棍球 0.792717
网球 0.792332
排球 0.789665
手球 0.780589
田径 0.780279
桌球 0.778775
举重 0.776161
沙滩排球 0.775708
乒乓球队 0.772797
// "男乒乓球" 并不存在, fasttext 仍然可以得到合理结果
Query word? 男乒乓球
乒乓球 0.767142
中国男子乒乓球队 0.725763
兵乓球 0.691391
体操 0.688922
张怡宁 0.688578
陈若琳 0.681761
跳水队 0.678217
打乒乓球 0.677547
王楠 0.671578
吴敏霞 0.669105

六. 总结

fasttext 是 facebook 开源的关于文本表达和文本分类的计算库。fasttext 结合词的子串信息计算词表达,提高了对微变形词汇的学习。针对文本分类模型, fasttext 使用了子空间量化和字典裁剪的策略,在不损失模型精度的情况下,将模型大小缩减数百倍。

在 fasttext 之上,可以做进一步优化,一个方向是在文本分类模型中,文本表达用类似 tf-idf 的方式对词进行加权平均。在字典裁剪算法上,一个涵盖所有文本的词并不一定有区分能力,比如 "the" 这个单词,可以尝试从保留区分能力的视角来保留字典。

七. 引用

[1] https://github.com/facebookresearch/fastText
[2] Bojanowski, Piotr and Grave, Edouard and Joulin, Armand and Mikolov, Tomas. "Enriching Word Vectors with Subword Information".
[3] Joulin, Armand and Grave, Edouard and Bojanowski, Piotr and Mikolov, Tomas. "Bag of Tricks for Efficient Text Classification".
[4]Joulin, Armand and Grave, Edouard and Bojanowski, Piotr and Douze, Matthijs and Jegou, Herve and Mikolov, Tomas. "FastText.zip: Compressing text classification models".
[5] Mikolov, Tomas, et al. "Efficient Estimation of Word Representations in Vector Space".
[6] Jeffrey Pennington, Richard Socher, and Christopher D. Manning. 2014. "GloVe: Global Vectors for Word Representation".
[7] Xinxiong Chen, Lei Xu, Zhiyuan Liu, Maosong Sun, and Huanbo Luan. 2015. Joint learning of character and word embeddings. In Proc. IJCAI.
[8] Product quantization for nearest neighbor search. H Jegou, M Douze, C Schmid. IEEE Transactions on Pattern Analysis and Machine Intelligence
[9] https://dumps.wikimedia.org/zhwiki/
[10] https://github.com/facebookresearch/fastText/blob/master/pretrained-vectors.md

作者:machinelearning
链接:https://www.jianshu.com/p/9ea0d69dd55e
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

FastText 分析与实践的更多相关文章

  1. Log4j2分析与实践

    当前网络上关于Log4j2的中文文章比较零散,这里整理了一下关于Log4j2比较全面的一些文章,供广大技术人员参考 Log4j2分析与实践-认识Log4j2 Log4j2分析与实践-架构 Log4j2 ...

  2. 苏宁基于Spark Streaming的实时日志分析系统实践 Spark Streaming 在数据平台日志解析功能的应用

    https://mp.weixin.qq.com/s/KPTM02-ICt72_7ZdRZIHBA 苏宁基于Spark Streaming的实时日志分析系统实践 原创: AI+落地实践 AI前线 20 ...

  3. 《Linux内核分析》实践4

    <Linux内核分析> 实践四--ELF文件格式分析 20135211李行之 一.概述 1.ELF全称Executable and Linkable Format,可执行连接格式,ELF格 ...

  4. 转:fastText原理及实践(达观数据王江)

    http://www.52nlp.cn/fasttext 1条回复 本文首先会介绍一些预备知识,比如softmax.ngram等,然后简单介绍word2vec原理,之后来讲解fastText的原理,并 ...

  5. 自定义View系列教程04--Draw源码分析及其实践

    深入探讨Android异步精髓Handler 站在源码的肩膀上全解Scroller工作机制 Android多分辨率适配框架(1)- 核心基础 Android多分辨率适配框架(2)- 原理剖析 Andr ...

  6. Supervisor行为分析和实践

    1.简介     Erlang要编写高容错性.稳定性的系统,supervisor就是用来解决这一问题的核心思想.通过建立一颗监控树,来组织进程之间的关系,通过确定重启策略.子进程说明书等参数信息来确定 ...

  7. Gen_server行为分析与实践

    1.简介 Gen_server实现了通用服务器client_server原理,几个不同的客户端去分享服务端管理的资源(如图),gen_server提供标准的接口函数和包含追踪功能以及错误报告来实现通用 ...

  8. AWVS结果分析与实践-XSS

      今天趁着老师接项目,做了一丢丢实践,以下是一点点感触.     都知道AWVS是神器,可是到我手里就是不灵.拿了它扫了一个URL,结果提示XSS漏洞,实践没反应,只好愉快地享受了过程.来看看.   ...

  9. 基于redis的分布式锁的分析与实践

    ​ 前言:在分布式环境中,我们经常使用锁来进行并发控制,锁可分为乐观锁和悲观锁,基于数据库版本戳的实现是乐观锁,基于redis或zookeeper的实现可认为是悲观锁了.乐观锁和悲观锁最根本的区别在于 ...

随机推荐

  1. assert.notDeepEqual()

    assert.notDeepEqual(actual, expected[, message]) 深度地不相等比较测试,与 assert.deepEqual() 相反. const assert = ...

  2. Adversarial Auto-Encoders

    目录 Another Approach: q(z)->p(z) Intuitively comprehend KL(p|q) Minimize KL Divergence How to comp ...

  3. python 网络编程基础

    1. 内容回顾补充 [] [^] 带有特殊意义的元字符到字符组内大部分都会取消它的特殊意义. 会取消的: [()+*.] -[(-)] -的位置决定了它的意义,写在字符组的第一个位置/最后一个位置就表 ...

  4. Django-rest_framework中利用jwt登录验证时,自定义返回凭证和登录校验支持手机号

    安装 pip install djangorestframework-jwt 在Django.settings中配置 REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATIO ...

  5. http chunked 理解

    https://imququ.com/post/transfer-encoding-header-in-http.html #! /usr/bin/python #coding:utf8 import ...

  6. Overload重載和Override重写的区别。Overloaded的方法是否可以改变返回值的类型?

    Overload是重载的意思,Override是覆盖的意思,也就是重写. 重载Overload表示同一个类中可以有多个名称相同的方法,但这些方法的参数列表各不相同(即参数个数或类型不同). 重写Ove ...

  7. jquery对JSON字符串的解析--eval函数

    jquery eval解析JSON中的注意点介绍----https://www.jb51.net/article/40842.htm

  8. 添物零基础到架构师(基础篇) - JavaScript

    JavaScript是什么? JavaScript是web开发必须学习的,ECMAScript是其规则来源. JavaScript的历史 Developed by Brendan Eich of Ne ...

  9. [luoguP1056] 排座椅(sort + 模拟)

    传送门 nc题,一直sort就过了 代码 #include <cstdio> #include <iostream> #include <algorithm> #d ...

  10. node.js 读取文件--createReadStream

    createReadStream 是fs模块里面读流的一个方法 这个方法基于fs模块的,所以我们先要引进fs模块 let fs=require("fs"); createReadS ...