AllenNLP入门笔记第二篇,来自官方tutorial:Using AllenNLP as a Library (Part 1) - Datasets and Models。之前的样例代码只是一个Python文件,虽然已经包含了一个深度学习自然语言处理项目的全部,但是不够详细,只是提供一个感性认识。重点就是要实现两个类,DatasetReader和Model。
这篇开始,跟随官方tutorial,更详细地学习AllenNLP项目地实现。这回是一个文档分类任务,将学术论文进行分类。
安装AllenNLP就略过了,就用Miniconda创建环境,安装PyTorch(用Conda安装是为了自动配置好CUDA环境),用pip安装AllenNLP。
许多项目中会看到requirements.txt
文件,这个文件可以用pip freeze > requirements.txt
命令生成,用pip install -r requirements.txt
安装项目的依赖。但是好像这个生成依赖文件的命令会包含环境中其他多余包(难道要手写吗?应该有根据Python项目直接生成的吧)。
allennlp==0.8.1
pytest-pythonpath
# Checks style, syntax, and other useful errors.
pylint==1.8.1
# Static type checking
mypy==0.521
# Coverage reports
pytest-cov
codecov
构建模型部分被放到了另一篇文章中,整个项目的简易版的项目文件组织如下,数据读取模块、模型模块、预测模块,各自一个文件夹。
my_library/
├── dataset_readers
│ ├── __init__.py
│ └── semantic_scholar_papers.py
├── __init__.py
├── models
│ ├── academic_paper_classifier.py
│ └── __init__.py
└── predictors
├── __init__.py
└── paper_classifier_predictor.py
3 directories, 7 files
再__init__
模块中,引入所有的模块:
from my_library.dataset_readers import *
from my_library.models import *
from my_library.predictors import *
DatasetReader
还没有说数据呢,这个任务的目标是将论文(包含标题和摘要)进行分类,总共三类(人工智能、机器学习和自然语言处理)。数据是用JSON格式组织的,举个栗子:
{
"title": "A review of Web searching studies and a framework for future research",
"paperAbstract": "Research on Web searching is at an incipient stage. ...",
"venue": "{AI|ML|ACL}"
}
每个样例至少三个字段,标题、论文摘要、分册(vene)。数据来源是Semantic Scholar提供的开放语料库。
DatasetReader
这里为DatasetReader
写了一个测试,虽然这不是必须的,但是用来了解AllenNLP自带的测试模块还是很有用的。
from allennlp.common.testing import AllenNlpTestCase
from allennlp.common.util import ensure_list
from my_library.dataset_readers import SemanticScholarDatasetReader
class TestSemanticScholarDatasetReader(AllenNlpTestCase):
def test_read_from_file(self):
reader = SemanticScholarDatasetReader()
instances = ensure_list(reader.read('tests/fixtures/s2_papers.jsonl'))
instance1 = {
"title": ["Interferring", "Discourse", "Relations", "in", "Context"],
"abstract": ["We", "investigate", "various", "contextual", "effects"],
"venue": "ACL"}
instance2 = {
"title": ["GRASPER", ":", "A", "Permissive", "Planning", "Robot"],
"abstract": ["Execut", "ion", "of", "classical", "plans"],
"venue": "AI"}
instance3 = {
"title": ["Route", "Planning", "under", "Uncertainty", ":", "The", "Canadian",
"Traveller", "Problem"],
"abstract": ["The", "Canadian", "Traveller", "problem", "is"],
"venue": "AI"}
assert len(instances) == 10
fields = instances[0].fields
assert [t.text for t in fields["title"].tokens] == instance1["title"]
assert [t.text for t in fields["abstract"].tokens[:5]] == instance1["abstract"]
assert fields["label"].label == instance1["venue"]
fields = instances[1].fields
assert [t.text for t in fields["title"].tokens] == instance2["title"]
assert [t.text for t in fields["abstract"].tokens[:5]] == instance2["abstract"]
assert fields["label"].label == instance2["venue"]
fields = instances[2].fields
assert [t.text for t in fields["title"].tokens] == instance3["title"]
assert [t.text for t in fields["abstract"].tokens[:5]] == instance3["abstract"]
assert fields["label"].label == instance3["venue"]
这个测试类继承自AllenNlpTestCase这个基类,主要就是把DatasetReader
读取的数据转换为样本的列表(因为read函数有可能返回的是一个迭代器),然后用assert检验。从测试代码就能看出来,我们要求DatasetReader
处理出的Instance
的fields
中有title、abstract和label字段,这三个字段又都有tokens这个属性(字符串列表或者字符串)。根据测试文件可以帮助明确DataReader
的功能。
DatasetReader
from typing import Dict
import json
import logging
from overrides import overrides
from allennlp.common.file_utils import cached_path
from allennlp.data.dataset_readers.dataset_reader import DatasetReader
from allennlp.data.fields import LabelField, TextField
from allennlp.data.instance import Instance
from allennlp.data.tokenizers import Tokenizer, WordTokenizer
from allennlp.data.token_indexers import TokenIndexer, SingleIdTokenIndexer
logger = logging.getLogger(__name__) # pylint: disable=invalid-name
@DatasetReader.register("s2_papers")
class SemanticScholarDatasetReader(DatasetReader):
def __init__(self,
lazy: bool = False,
tokenizer: Tokenizer = None,
token_indexers: Dict[str, TokenIndexer] = None) -> None:
super().__init__(lazy)
self._tokenizer = tokenizer or WordTokenizer()
self._token_indexers = token_indexers or {
"tokens": SingleIdTokenIndexer()}
@overrides
def _read(self, file_path):
with open(cached_path(file_path), "r") as data_file:
logger.info("Reading instances from lines in file at: %s", file_path)
for line in data_file:
line = line.strip("\n")
if not line:
continue
paper_json = json.loads(line)
title = paper_json['title']
abstract = paper_json['paperAbstract']
venue = paper_json['venue']
yield self.text_to_instance(title, abstract, venue)
@overrides
def text_to_instance(self, title: str, abstract: str, venue: str = None) -> Instance: # type: ignore
# pylint: disable=arguments-differ
tokenized_title = self._tokenizer.tokenize(title)
tokenized_abstract = self._tokenizer.tokenize(abstract)
title_field = TextField(tokenized_title, self._token_indexers)
abstract_field = TextField(tokenized_abstract, self._token_indexers)
fields = {
'title': title_field, 'abstract': abstract_field}
if venue is not None:
fields['label'] = LabelField(venue)
return Instance(fields)
和之前的样例代码一样,DatasetReader
类也是三个函数: __init__
、_read
、text_to_instance
。这里用了三个Python中的特性装饰器、类方法重载和注册器。
_read
函数这里用了python自带的日志模块logging,以前一直都没有用过,需要了解了解。
其实这个函数很简单,就是一行行读取整个数据的json文件,调用同一个类中的text_to_instance
生成Instance
。当然,这里为了防止内存占用过多,用的是生成器,而不是直接返回一个包含所有实例的列表。
text_to_instance
这个函数才是重点。实际上,如果规模不大,这个函数完全可以整合到_read
函数中。这里我们需要的是一个分词器self._tokenizer
和一个分词索引self._token_indexers
,类中的成员用单下划线开头是为了声明其为内部成员,一般不用于外部调用(虽然实际上Python是不管的,这只是一个命名的规范而已)。上一篇中提到过AllenNLP中的数据组织方法中,每个样本是Instance
,每个Instance
中有不同的Fields
,有TextField
,有LabelFields
(就是目标)等。
所以这个函数实际上也很简单,就是对title和abstract进行分词和索引,和作为label的vene一样都组织成field。这些fields以字典的方式组织成Instance
后返回。
__init__
函数根据上面两个函数,我们可以看出来初始化函数需要的参数和成员,即一个分词器和一个分词索引。分词器使用外部传入的参数,若缺省则使用SpacyTokenizer
;分词索引同样来自外部参数(一个字符串到TokenIndexer
的字典映射),若缺省则使用只包含“token”到SingleIdTokenIndexer
的字典(这些分词器和分词索引之后再详细分辨)。
实际上,用户不需要自己写整个DatasetReader
,可以写一个JSON配置文件说明结构(模型、训练器也是如此,甚至整个用AllenNLP的项目都可以用JSON文件),让AllenNLP自行生成相应的DatasetReader。因此,需要在DatasetReader的子类声明前注册函数名字,以便AllenNLP能够明确类的名字,否则Python文件被编译成虚拟机的字节码之后就不知道变成什么名字了。JSON文件的写法往后再详述。
文档中还提到可以用pytest
库很方便地对DatasetReader
进行测试,Python也有自带的单元测试库unittest
。(不了解,后面再学吧。)
from allennlp.common.testing import ModelTestCase
class AcademicPaperClassifierTest(ModelTestCase):
def setUp(self):
super().setUp()
self.set_up_model('tests/fixtures/academic_paper_classifier.json',
'tests/fixtures/s2_papers.jsonl')
def test_model_can_train_save_and_load(self):
self.ensure_model_can_train_save_and_load(self.param_file)
这里用的是测试模块是allennlp.common.testing.ModelTestCase
,继承自AllenNlpTestCase
,AllenNlpTestCase
继承自Python标准库unittest.TestCase
。这里在setUp函数中添加上用于测试的两个文件,一个是用于训练的参数,一个是用于测试的小数据集(每行一个JSON格式的样本,JSONL格式)。
另一个函数是test_model_can_train_save_and_load
,调用ModelTestCase.ensure_model_can_train_save_and_load
。
这个测试类很重要,保证你用小部分数据集就先测试一下模型没有写错。
from typing import Dict, Optional
import numpy
from overrides import overrides
import torch
import torch.nn.functional as F
from allennlp.common.checks import ConfigurationError
from allennlp.data import Vocabulary
from allennlp.modules import FeedForward, Seq2VecEncoder, TextFieldEmbedder
from allennlp.models.model import Model
from allennlp.nn import InitializerApplicator, RegularizerApplicator
from allennlp.nn import util
from allennlp.training.metrics import CategoricalAccuracy
@Model.register("paper_classifier")
class AcademicPaperClassifier(Model):
def __init__(self, vocab: Vocabulary,
text_field_embedder: TextFieldEmbedder,
title_encoder: Seq2VecEncoder,
abstract_encoder: Seq2VecEncoder,
classifier_feedforward: FeedForward,
initializer: InitializerApplicator = InitializerApplicator(),
regularizer: Optional[RegularizerApplicator] = None) -> None:
super(AcademicPaperClassifier, self).__init__(vocab, regularizer)
self.text_field_embedder = text_field_embedder
self.num_classes = self.vocab.get_vocab_size("labels")
self.title_encoder = title_encoder
self.abstract_encoder = abstract_encoder
self.classifier_feedforward = classifier_feedforward
if text_field_embedder.get_output_dim() != title_encoder.get_input_dim():
raise ConfigurationError("The output dimension of the text_field_embedder must match the "
"input dimension of the title_encoder. Found {} and {}, "
"respectively.".format(text_field_embedder.get_output_dim(),
title_encoder.get_input_dim()))
if text_field_embedder.get_output_dim() != abstract_encoder.get_input_dim():
raise ConfigurationError("The output dimension of the text_field_embedder must match the "
"input dimension of the abstract_encoder. Found {} and {}, "
"respectively.".format(text_field_embedder.get_output_dim(),
abstract_encoder.get_input_dim()))
self.metrics = {
"accuracy": CategoricalAccuracy(),
"accuracy3": CategoricalAccuracy(top_k=3)
}
self.loss = torch.nn.CrossEntropyLoss()
initializer(self)
@overrides
def forward(self, # type: ignore
title: Dict[str, torch.LongTensor],
abstract: Dict[str, torch.LongTensor],
label: torch.LongTensor = None) -> Dict[str, torch.Tensor]:
embedded_title = self.text_field_embedder(title)
title_mask = util.get_text_field_mask(title)
encoded_title = self.title_encoder(embedded_title, title_mask)
embedded_abstract = self.text_field_embedder(abstract)
abstract_mask = util.get_text_field_mask(abstract)
encoded_abstract = self.abstract_encoder(embedded_abstract, abstract_mask)
logits = self.classifier_feedforward(torch.cat([encoded_title, encoded_abstract], dim=-1))
output_dict = {
'logits': logits}
if label is not None:
loss = self.loss(logits, label)
for metric in self.metrics.values():
metric(logits, label)
output_dict["loss"] = loss
return output_dict
@overrides
def decode(self, output_dict: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor]:
class_probabilities = F.softmax(output_dict['logits'], dim=-1)
output_dict['class_probabilities'] = class_probabilities
predictions = class_probabilities.cpu().data.numpy()
argmax_indices = numpy.argmax(predictions, axis=-1)
labels = [self.vocab.get_token_from_index(x, namespace="labels")
for x in argmax_indices]
output_dict['label'] = labels
return output_dict
@overrides
def get_metrics(self, reset: bool = False) -> Dict[str, float]:
return {
metric_name: metric.get_metric(reset) for metric_name, metric in self.metrics.items()}
模型类中有四个函数:__init__
、forward
、decode
和get_metrics
。
和DatasetReader
一样,开头注册模型以保证我们可以用配置文件进行调用。这里需要一个Vocabulary
单词表来将字符串映射到整数。这里用到的Vocabulary.get_vocab_size
函数在文档里没有,它是一个获取单词表大小的函数。本例中,不同的token到索引的映射有自己的namespace
,title、abstract和label都有相应的映射。
其他参数包括一个嵌入层TextFieldEmbedder
将DatasetReader
得来的整数索引转换为张量(实际上就是一个大映射,只不过这个层经常是预训练好的);两个分别针对title和abstract的序列编码层Seq2VecEncoder
;一个将编码结果计算为结果的前向传播分类器FeedForward
;一个初始化器和一个正则化器。
除此之外,还有一个评测metrics字典(这里包含了两种评测,这个评测用的准确率accuracy不是最终测试模型中用的唯一指标,更不是用于训练的指标,只是用于训练阶段的参考),一个用于后向传播的损失函数。
前向传播函数forward
就是模型的计算过程,本例模型的计算流程很简单,就是title和abstract经过编码层的编码后相连接,然后过一个分类器。如果输入的数据中包含label,则计算一下loss
和metrics
。(这里的mask
是什么用途不清楚。)
这里的输出的结果需要用softmax
变成概率分布,使用输出结果的每一位表示每一种label的概率。整体返回的output_dict
仍然用字典表示。
评测函数metrics
看起来比较简单,就是调用每个评测指标计算结果存到一个字典中。
解码函数decode
是将前向传播的结果(可能包含其他结果)进行解码,把整数转换为可读性更好的字符串。
上一篇用的是AllenNLP的training
库,这回采用配置文件。实际上两者是相通的,只不过用Python的自由度更高,用JSON(实际上是一种JSON的超集)的结构化更强。
{
"dataset_reader": {
"type": "s2_papers"
},
"train_data_path": "https://s3-us-west-2.amazonaws.com/allennlp/datasets/academic-papers-example/train.jsonl",
"validation_data_path": "https://s3-us-west-2.amazonaws.com/allennlp/datasets/academic-papers-example/dev.jsonl",
"model": {
"type": "paper_classifier",
"text_field_embedder": {
"token_embedders": {
"tokens": {
"type": "embedding",
"pretrained_file": "https://s3-us-west-2.amazonaws.com/allennlp/datasets/glove/glove.6B.100d.txt.gz",
"embedding_dim": 100,
"trainable": false
}
}
},
"title_encoder": {
"type": "lstm",
"bidirectional": true,
"input_size": 100,
"hidden_size": 100,
"num_layers": 1,
"dropout": 0.2
},
"abstract_encoder": {
"type": "lstm",
"bidirectional": true,
"input_size": 100,
"hidden_size": 100,
"num_layers": 1,
"dropout": 0.2
},
"classifier_feedforward": {
"input_dim": 400,
"num_layers": 2,
"hidden_dims": [200, 3],
"activations": ["relu", "linear"],
"dropout": [0.2, 0.0]
}
},
"iterator": {
"type": "bucket",
"sorting_keys": [["abstract", "num_tokens"], ["title", "num_tokens"]],
"batch_size": 64
},
"trainer": {
"num_epochs": 40,
"patience": 10,
"cuda_device": -1,
"grad_clipping": 5.0,
"validation_metric": "+accuracy",
"optimizer": {
"type": "adagrad"
}
}
}
Data
和Trainer
配置文件中关于数据和训练器的有dataset_reader
、train_data_path
、validation_data_path
、iterator
和trainer
。本例中给出的两个训练与验证数据的路径都是网络路径,AllenNLP会将其缓存的(应该也是可以指定本地路径的,也可以指定缓存的路径)。
iterator
是用来说明trainer
如何对数据进行遍历的,本例中采用bucket
类型的迭代器,默认会根据最大输入长度填充batch
,如果提供一个排序的索引则会提高计算效率(不明白这里的意思)。
trainer
中patience
参数为10(early stopping的耐心值),结合validation_metric
为+accuracy
(加号表示期望增长),应该说明如果准确率10轮没有增长则提前结束训练。
值得注意的是dataset_reader
中的type
属性,这和Python文件中注册的DatasetReader
类的名字是对应的。
Model
模型的JSON配置除了和注册名相同的paper包括四部分,对应模型类中的四部分:词嵌入层、title和abstract两个编码层以及一个分类层。这里注意一下type
这个属性,lstm
在这里指的是PyTorch
的torch.nn.LSTM
外套一个allennlp.modules.pytorch_seq2vec_wrapper
以保证其被转为AllenNLP的Seq2VecWrapper
。
感觉这种方式虽然调整一些参数,调换一些模块方便,但是写起来好像并不方便。。。
AllenNLP的命令行帮助提示:
allennlp -h
usage: allennlp
Run AllenNLP
optional arguments:
-h, --help show this help message and exit
--version show program's version number and exit
Commands:
configure Run the configuration wizard.
train Train a model.
evaluate Evaluate the specified model + dataset.
predict Use a trained model to make predictions.
make-vocab Create a vocabulary.
elmo Create word vectors using a pretrained ELMo model.
fine-tune Continue training a model on a new dataset.
dry-run Create a vocabulary, compute dataset statistics and other
training utilities.
test-install
Run the unit tests.
find-lr Find a learning rate range.
print-results
Print results from allennlp serialization directories to the
console.
allennlp train -h
usage: allennlp train [-h] -s SERIALIZATION_DIR [-r] [-f] [-o OVERRIDES]
[--file-friendly-logging]
[--cache-directory CACHE_DIRECTORY]
[--cache-prefix CACHE_PREFIX]
[--include-package INCLUDE_PACKAGE]
param_path
Train the specified model on the specified dataset.
positional arguments:
param_path path to parameter file describing the model to be
trained
optional arguments:
-h, --help show this help message and exit
-s SERIALIZATION_DIR, --serialization-dir SERIALIZATION_DIR
directory in which to save the model and its logs
-r, --recover recover training from the state in serialization_dir
-f, --force overwrite the output directory if it exists
-o OVERRIDES, --overrides OVERRIDES
a JSON structure used to override the experiment
configuration
--file-friendly-logging
outputs tqdm status on separate lines and slows tqdm
refresh rate
--cache-directory CACHE_DIRECTORY
Location to store cache of data preprocessing
--cache-prefix CACHE_PREFIX
Prefix to use for data caching, giving current
parameter settings a name in the cache, instead of
computing a hash
--include-package INCLUDE_PACKAGE
additional packages to include
allennlp train \
experiments/venue_classifier.json \
-s /tmp/venue_output_dir \
--include-package my_library
训练命令中的首要参数是上述的配置文件,-s表示保存模型和日志的目录,–include-package命令包含的my_library中保存我们的DatasetReader、Model和Predictor(这样才能通过JSON配置文件找到这些注册过名字的类)。
感觉这样用大篇幅JSON实在是不好写,实现简单的模型问题不大,但是复杂的、定制化的还是Python好用。看看allennlp.commands库中的模块,比如allennlp.commands.train、allennlp.commands.evaluate等等。
Predictor
有了上文的DatasetReader
和模型中的forward
,预测函数只需要注册相应的模型名字,重载predict_json
函数。
from overrides import overrides
from allennlp.common.util import JsonDict
from allennlp.data import Instance
from allennlp.predictors.predictor import Predictor
@Predictor.register('paper-classifier')
class PaperClassifierPredictor(Predictor):
""""Predictor wrapper for the AcademicPaperClassifier"""
def predict_json(self, inputs: JsonDict) -> JsonDict:
instance = self._json_to_instance(inputs)
output_dict = self.predict_instance(instance)
# label_dict will be like {0: "ACL", 1: "AI", ...}
label_dict = self._model.vocab.get_index_to_token_vocabulary('labels')
# Convert it to list ["ACL", "AI", ...]
all_labels = [label_dict[i] for i in range(len(label_dict))]
output_dict["all_labels"] = all_labels
return output_dict
@overrides
def _json_to_instance(self, json_dict: JsonDict) -> Instance:
title = json_dict['title']
abstract = json_dict['paperAbstract']
return self._dataset_reader.text_to_instance(title=title, abstract=abstract)
后续会逐一看看AllenNLP的各个模块的API和源代码,在那里API里能够看到,allennlp.commands.predict
(allennlp predict
命令就是调用的这个模块)是allennlp.predictors.predictor
的一层包装。在allennlp.predictors.predictor
定义中Predictor
类有两个参数,分别是模型和DatasetReader
,其中有一个函数是predict_json
(就是这里重载的函数)。
Predictor
同样也要为Predictor
配备一个测试文件。
from unittest import TestCase
from pytest import approx
from allennlp.models.archival import load_archive
from allennlp.predictors import Predictor
# required so that our custom model + predictor + dataset reader
# will be registered by name
import my_library
class TestPaperClassifierPredictor(TestCase):
def test_uses_named_inputs(self):
inputs = {
"title": "Interferring Discourse Relations in Context",
"paperAbstract": (
"We investigate various contextual effects on text "
"interpretation, and account for them by providing "
"contextual constraints in a logical theory of text "
"interpretation. On the basis of the way these constraints "
"interact with the other knowledge sources, we draw some "
"general conclusions about the role of domain-specific "
"information, top-down and bottom-up discourse information "
"flow, and the usefulness of formalisation in discourse theory."
)
}
archive = load_archive('tests/fixtures/model.tar.gz')
predictor = Predictor.from_archive(archive, 'paper-classifier')
result = predictor.predict_json(inputs)
label = result.get("label")
assert label in {
'AI', 'ML', 'ACL'}
all_labels = result.get("all_labels")
assert all_labels == ['AI', 'ACL', 'ML']
class_probabilities = result.get("class_probabilities")
assert class_probabilities is not None
assert all(cp > 0 for cp in class_probabilities)
assert sum(class_probabilities) == approx(1.0)
测试文件主要就是加载模型文件,加载predictor
,检验一下预测结果中有没有满足每一项的概率大于0,加起来是否为1。这个测试类是继承自TestCase
的,所以只是自行定制的。当然,要注意将my_library
模块引入,里面包含着之前实现的模型和预测器。
执行预测就使用allennlp predict
,注意和训练的命令一样,要包含my_library
库(里面有我们的DatasetReader
、Model
和predict
),还要指明predictor
的注册名字。
usage: allennlp [command] predict [-h]
[--output-file OUTPUT_FILE]
[--batch-size BATCH_SIZE]
[--silent]
[--cuda-device CUDA_DEVICE]
[-o OVERRIDES]
[--include-package INCLUDE_PACKAGE]
[--predictor PREDICTOR]
archive_file input_file
allennlp predict \
tests/fixtures/model.tar.gz \
tests/fixtures/s2_papers.jsonl \
--include-package my_library \
--predictor paper-classifier
通过--output-file
指明输出结果到文件,结果大致如下:
prediction: {
"instance": {
"logits": [0.008737504482269287, 0.22074833512306213, -0.005263201892375946], "class_probabilities": [0.31034138798713684, 0.38363200426101685, 0.3060266375541687], "label": "ACL"}, "all_labels": ["AI", "ACL", "ML"]}
Web Demo就是用来将model、trainer参数和结果展示在浏览器中,但是这个功能还在开发中,allennlp-server还没有并入allennlp库中。最关键的是,我安装运行失败了(((φ(◎ロ◎;)φ))),所以留个坑吧。
看看完整的目录吧
.
├── build_tools
│ └── travis
│ ├── after_success.sh
│ ├── install.sh
│ └── test_script.sh
├── codecov.yml
├── experiments
│ ├── venue_classifier_boe.json
│ └── venue_classifier.json
├── my_library
│ ├── dataset_readers
│ │ ├── __init__.py
│ │ ├── __pycache__
│ │ │ ├── __init__.cpython-37.pyc
│ │ │ └── semantic_scholar_papers.cpython-37.pyc
│ │ └── semantic_scholar_papers.py
│ ├── __init__.py
│ ├── models
│ │ ├── academic_paper_classifier.py
│ │ ├── __init__.py
│ │ └── __pycache__
│ │ ├── academic_paper_classifier.cpython-37.pyc
│ │ └── __init__.cpython-37.pyc
│ ├── predictors
│ │ ├── __init__.py
│ │ ├── paper_classifier_predictor.py
│ │ └── __pycache__
│ │ ├── __init__.cpython-37.pyc
│ │ └── paper_classifier_predictor.cpython-37.pyc
│ └── __pycache__
│ └── __init__.cpython-37.pyc
├── pytest.ini
├── README.md
├── requirements.txt
├── static_html
│ ├── demo.css
│ └── index.html
├── tests
│ ├── dataset_readers
│ │ ├── __init__.py
│ │ ├── __pycache__
│ │ │ ├── __init__.cpython-37.pyc
│ │ │ └── semantic_scholar_dataset_reader_test.cpython-37-pytest-5.3.5.pyc
│ │ └── semantic_scholar_dataset_reader_test.py
│ ├── fixtures
│ │ ├── academic_paper_classifier.json
│ │ ├── model.tar.gz
│ │ └── s2_papers.jsonl
│ ├── models
│ │ ├── academic_paper_classifier_test.py
│ │ ├── __init__.py
│ │ └── __pycache__
│ │ ├── academic_paper_classifier_test.cpython-37-pytest-5.3.5.pyc
│ │ └── __init__.cpython-37.pyc
│ └── predictors
│ ├── predictor_test.py
│ └── __pycache__
│ └── predictor_test.cpython-37-pytest-5.3.5.pyc
└── tmp
├── prediction.txt
└── venue_output_dir
├── best.th
├── config.json
├── log
│ ├── train
│ │ └── events.out.tfevents.1583980176.xiaoyong-huawei-laptop
│ └── validation
│ └── events.out.tfevents.1583980176.xiaoyong-huawei-laptop
├── metrics_epoch_0.json
├── metrics.json
├── model_state_epoch_0.th
├── model.tar.gz
├── stderr.log
├── stdout.log
├── training_state_epoch_0.th
└── vocabulary
├── labels.txt
├── non_padded_namespaces.txt
└── tokens.txt
26 directories, 53 files
目录中最主要的就是这个my_library目录,里面有我们的模型、DatasetReader和Predictor;其次是experiments,里面是一份训练的配置文件(一份就够了,多余的是不同的实验设置);然后是test目录,其中是单元测试文件,不过这里用的是bash脚本去调用的,实际上也可以不管;tmp目录中是训练的日志、模型文件以及预测的结果输出;那个build_tools文件是用bash脚本来配置环境、测试用的,其实不太需要,其他的也不太重要了。整个目录是以最终要打包为目的写的,实际上也不用这么规范,保证my_library、experiments和test三个目录就够了,当然数据可以单独建立一个目录。