Add model
Browse files- config.json +22 -0
- configuration.py +13 -0
- model.py +35 -0
- pipeline.py +116 -0
- pytorch_model.bin +3 -0
- special_tokens_map.json +7 -0
- summary.py +106 -0
- tokenizer_config.json +15 -0
- transformerutils.py +65 -0
- utilities.py +5 -0
- vocab.txt +0 -0
config.json
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"architectures": [
|
3 |
+
"BERTSummarizer"
|
4 |
+
],
|
5 |
+
"auto_map": {
|
6 |
+
"AutoConfig": "configuration.ExtSummConfig",
|
7 |
+
"AutoModel": "model.BERTSummarizer"
|
8 |
+
},
|
9 |
+
"custom_pipelines": {
|
10 |
+
"summarization": {
|
11 |
+
"impl": "pipeline.ExtSummPipeline",
|
12 |
+
"pt": [
|
13 |
+
"AutoModel"
|
14 |
+
],
|
15 |
+
"tf": []
|
16 |
+
}
|
17 |
+
},
|
18 |
+
"input_size": 512,
|
19 |
+
"model_type": "pubmedbert-bio-ext-summ",
|
20 |
+
"torch_dtype": "float32",
|
21 |
+
"transformers_version": "4.30.2"
|
22 |
+
}
|
configuration.py
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from transformers import PretrainedConfig
|
2 |
+
|
3 |
+
|
4 |
+
class ExtSummConfig(PretrainedConfig):
|
5 |
+
model_type = "pubmedbert-bio-ext-summ"
|
6 |
+
|
7 |
+
def __init__(
|
8 |
+
self,
|
9 |
+
input_size: int = 512,
|
10 |
+
**kwargs
|
11 |
+
):
|
12 |
+
self.input_size = input_size
|
13 |
+
super().__init__(**kwargs)
|
model.py
ADDED
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import torch
|
2 |
+
from .transformerutils import TransformerInterEncoder
|
3 |
+
from transformers import PreTrainedModel, AutoModel, BertConfig
|
4 |
+
from .configuration import ExtSummConfig
|
5 |
+
|
6 |
+
|
7 |
+
|
8 |
+
class BERTSummarizer(PreTrainedModel):
|
9 |
+
config_class = ExtSummConfig
|
10 |
+
|
11 |
+
def __init__(self, config):
|
12 |
+
super().__init__(config)
|
13 |
+
self.bert = AutoModel.from_config(BertConfig.from_pretrained("microsoft/BiomedNLP-PubMedBERT-base-uncased-abstract-fulltext"))
|
14 |
+
self.input_size = config.input_size
|
15 |
+
self.encoder = TransformerInterEncoder(self.bert.config.hidden_size, max_len=512)
|
16 |
+
|
17 |
+
def forward(self, batch):
|
18 |
+
document_ids = batch["ids"].to(self.bert.device)
|
19 |
+
segments_ids = batch["segments_ids"].to(self.bert.device)
|
20 |
+
clss_mask = batch["clss_mask"].to(self.bert.device)
|
21 |
+
attn_mask = batch["attn_mask"].to(self.bert.device)
|
22 |
+
|
23 |
+
tokens_out, _ = self.bert(input_ids=document_ids, token_type_ids=segments_ids, attention_mask=attn_mask, return_dict=False)
|
24 |
+
out = []
|
25 |
+
logits_out = []
|
26 |
+
|
27 |
+
for i in range(len(tokens_out)): # Batch handling
|
28 |
+
clss_out = tokens_out[i][clss_mask[i], :]
|
29 |
+
sentences_scores, logits = self.encoder(clss_out)
|
30 |
+
padding = torch.zeros(self.input_size - sentences_scores.shape[0]).to(sentences_scores.device)
|
31 |
+
|
32 |
+
out.append( torch.cat((sentences_scores, padding)) )
|
33 |
+
logits_out.append( torch.cat((logits, padding)) )
|
34 |
+
|
35 |
+
return torch.stack(out), torch.stack(logits_out)
|
pipeline.py
ADDED
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from transformers import Pipeline
|
2 |
+
import torch
|
3 |
+
from .utilities import padToSize
|
4 |
+
from .summary import select, splitDocument
|
5 |
+
|
6 |
+
|
7 |
+
"""
|
8 |
+
Generates the segments ids for BERT
|
9 |
+
"""
|
10 |
+
def generateSegmentIds(doc_ids, tokenizer):
|
11 |
+
# Alternating 0s and 1s
|
12 |
+
segments_ids = [0] * len(doc_ids)
|
13 |
+
curr_segment = 0
|
14 |
+
|
15 |
+
for i, token in enumerate(doc_ids):
|
16 |
+
segments_ids[i] = curr_segment
|
17 |
+
if token == tokenizer.vocab["[SEP]"]:
|
18 |
+
curr_segment = 1 - curr_segment
|
19 |
+
|
20 |
+
return segments_ids
|
21 |
+
|
22 |
+
|
23 |
+
class ExtSummPipeline(Pipeline):
|
24 |
+
"""
|
25 |
+
Extractive summarization pipeline
|
26 |
+
|
27 |
+
Inputs
|
28 |
+
------
|
29 |
+
inputs : dict
|
30 |
+
'sentences' : list[str]
|
31 |
+
Sentences of the document
|
32 |
+
|
33 |
+
strategy : str
|
34 |
+
Strategy to summarize the document:
|
35 |
+
- 'length': summary with a maximum length (strategy_args is the maximum length).
|
36 |
+
- 'count': summary with the given number of sentences (strategy_args is the number of sentences).
|
37 |
+
- 'ratio': summary proportional to the length of the document (strategy_args is the ratio [0, 1]).
|
38 |
+
- 'threshold': summary only with sentences with a score higher than a given value (strategy_args is the minimum score).
|
39 |
+
|
40 |
+
strategy_args : any
|
41 |
+
Parameters of the strategy.
|
42 |
+
|
43 |
+
Outputs
|
44 |
+
-------
|
45 |
+
selected_sents : list[str]
|
46 |
+
List of the selected sentences
|
47 |
+
|
48 |
+
selected_idxs : list[int]
|
49 |
+
List of the indexes of the selected sentences in the original input
|
50 |
+
"""
|
51 |
+
|
52 |
+
|
53 |
+
def _sanitize_parameters(self, **kwargs):
|
54 |
+
postprocess_kwargs = {}
|
55 |
+
|
56 |
+
if ("strategy" in kwargs and "strategy_args" not in kwargs) or ("strategy" not in kwargs and "strategy_args" in kwargs):
|
57 |
+
raise ValueError("`strategy` and `strategy_args` have to be both set")
|
58 |
+
if "strategy" in kwargs:
|
59 |
+
postprocess_kwargs["strategy"] = kwargs["strategy"]
|
60 |
+
if "strategy_args" in kwargs:
|
61 |
+
postprocess_kwargs["strategy_args"] = kwargs["strategy_args"]
|
62 |
+
|
63 |
+
return {}, {}, postprocess_kwargs
|
64 |
+
|
65 |
+
|
66 |
+
def preprocess(self, inputs):
|
67 |
+
sentences = inputs["sentences"]
|
68 |
+
|
69 |
+
# Tokenization and chunking
|
70 |
+
doc_tokens = self.tokenizer.tokenize( f"{self.tokenizer.sep_token}{self.tokenizer.cls_token}".join(sentences) )
|
71 |
+
doc_tokens = [self.tokenizer.cls_token] + doc_tokens + [self.tokenizer.sep_token]
|
72 |
+
doc_chunks = splitDocument(doc_tokens, self.tokenizer.cls_token, self.tokenizer.sep_token, self.model.config.input_size)
|
73 |
+
|
74 |
+
# Batch preparation
|
75 |
+
batch = {
|
76 |
+
"ids": [],
|
77 |
+
"segments_ids": [],
|
78 |
+
"clss_mask": [],
|
79 |
+
"attn_mask": [],
|
80 |
+
}
|
81 |
+
for chunk_tokens in doc_chunks:
|
82 |
+
doc_ids = self.tokenizer.convert_tokens_to_ids(chunk_tokens)
|
83 |
+
segment_ids = generateSegmentIds(doc_ids, self.tokenizer)
|
84 |
+
clss_mask = [True if token == self.tokenizer.cls_token_id else False for token in doc_ids]
|
85 |
+
attn_mask = [1 for _ in range(len(doc_ids))]
|
86 |
+
|
87 |
+
batch["ids"].append( padToSize(doc_ids, self.model.config.input_size, self.tokenizer.pad_token_id) )
|
88 |
+
batch["segments_ids"].append( padToSize(segment_ids, self.model.config.input_size, 0) )
|
89 |
+
batch["clss_mask"].append( padToSize(clss_mask, self.model.config.input_size, False) )
|
90 |
+
batch["attn_mask"].append( padToSize(attn_mask, self.model.config.input_size, 0) )
|
91 |
+
|
92 |
+
batch["ids"] = torch.as_tensor(batch["ids"])
|
93 |
+
batch["segments_ids"] = torch.as_tensor(batch["segments_ids"])
|
94 |
+
batch["clss_mask"] = torch.as_tensor(batch["clss_mask"])
|
95 |
+
batch["attn_mask"] = torch.as_tensor(batch["attn_mask"])
|
96 |
+
return { "inputs": batch, "sentences": sentences }
|
97 |
+
|
98 |
+
|
99 |
+
def _forward(self, args):
|
100 |
+
batch = args["inputs"]
|
101 |
+
sentences = args["sentences"]
|
102 |
+
out_predictions = torch.as_tensor([]).to(self.device)
|
103 |
+
|
104 |
+
self.model.eval()
|
105 |
+
with torch.no_grad():
|
106 |
+
batch_preds, _ = self.model(batch)
|
107 |
+
for i, clss_mask in enumerate(batch["clss_mask"]):
|
108 |
+
out_predictions = torch.cat((out_predictions, batch_preds[i][:torch.sum(clss_mask == True)]))
|
109 |
+
|
110 |
+
return { "predictions": out_predictions, "sentences": sentences }
|
111 |
+
|
112 |
+
|
113 |
+
def postprocess(self, args, strategy: str="count", strategy_args=3):
|
114 |
+
predictions = args["predictions"]
|
115 |
+
sentences = args["sentences"]
|
116 |
+
return select(sentences, predictions, strategy, strategy_args)
|
pytorch_model.bin
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:4564a38bbef76f25a17e7254197bddd1f0635e10b4674556f453f98bc1e38108
|
3 |
+
size 483701601
|
special_tokens_map.json
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"cls_token": "[CLS]",
|
3 |
+
"mask_token": "[MASK]",
|
4 |
+
"pad_token": "[PAD]",
|
5 |
+
"sep_token": "[SEP]",
|
6 |
+
"unk_token": "[UNK]"
|
7 |
+
}
|
summary.py
ADDED
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import torch
|
2 |
+
|
3 |
+
|
4 |
+
def _selectStrategyLength(sentences, predictions, max_length):
|
5 |
+
selected_sents = []
|
6 |
+
sents_priority = torch.argsort(predictions, descending=True)
|
7 |
+
summary_len = 0
|
8 |
+
i = 0
|
9 |
+
|
10 |
+
while (summary_len < max_length) and (i < len(sents_priority)):
|
11 |
+
if summary_len + len(sentences[sents_priority[i]]) < max_length:
|
12 |
+
selected_sents.append(sents_priority[i].item())
|
13 |
+
summary_len += len(sentences[sents_priority[i]])
|
14 |
+
i += 1
|
15 |
+
|
16 |
+
return sorted(selected_sents)
|
17 |
+
|
18 |
+
|
19 |
+
def _selectStrategyCount(sentences, predictions, num_sents):
|
20 |
+
selected_idxs = sorted(torch.topk(predictions, min(len(predictions), num_sents)).indices)
|
21 |
+
return [tensor.item() for tensor in selected_idxs]
|
22 |
+
|
23 |
+
|
24 |
+
def _selectStrategyRatio(sentences, predictions, ratio):
|
25 |
+
doc_length = sum([ len(sent) for sent in sentences ])
|
26 |
+
return _selectStrategyLength(sentences, predictions, doc_length*ratio)
|
27 |
+
|
28 |
+
|
29 |
+
def _selectStrategyThreshold(sentences, predictions, threshold):
|
30 |
+
return [i for i, score in enumerate(predictions) if score >= threshold]
|
31 |
+
|
32 |
+
|
33 |
+
def select(sentences, predictions, strategy, strategy_args):
|
34 |
+
selected_sents = []
|
35 |
+
|
36 |
+
if strategy == "length":
|
37 |
+
selected_sents = _selectStrategyLength(sentences, predictions, strategy_args)
|
38 |
+
elif strategy == "count":
|
39 |
+
selected_sents = _selectStrategyCount(sentences, predictions, strategy_args)
|
40 |
+
elif strategy == "ratio":
|
41 |
+
selected_sents = _selectStrategyRatio(sentences, predictions, strategy_args)
|
42 |
+
elif strategy == "threshold":
|
43 |
+
selected_sents = _selectStrategyThreshold(sentences, predictions, strategy_args)
|
44 |
+
else:
|
45 |
+
raise NotImplementedError(f"Unknown strategy {strategy}")
|
46 |
+
|
47 |
+
return [sentences[i] for i in selected_sents], selected_sents
|
48 |
+
|
49 |
+
|
50 |
+
|
51 |
+
"""
|
52 |
+
Splits a document in chunks of maximum a given size.
|
53 |
+
|
54 |
+
Parameters
|
55 |
+
----------
|
56 |
+
doc_tokens : str[]
|
57 |
+
List of the tokens of the document.
|
58 |
+
|
59 |
+
bos_token : str
|
60 |
+
Begin of sentence token.
|
61 |
+
|
62 |
+
eos_token : str
|
63 |
+
End of sentence token.
|
64 |
+
|
65 |
+
max_size : int
|
66 |
+
Maximum size of a chunk.
|
67 |
+
Returns
|
68 |
+
-------
|
69 |
+
chunks : str[][]
|
70 |
+
Splitted document.
|
71 |
+
"""
|
72 |
+
def splitDocument(doc_tokens, bos_token, eos_token, max_size):
|
73 |
+
def _findNextBOSFrom(start_idx):
|
74 |
+
for i in range(start_idx, len(doc_tokens)):
|
75 |
+
if doc_tokens[i] == bos_token:
|
76 |
+
return i
|
77 |
+
return -1
|
78 |
+
|
79 |
+
def _findPreviousEOSFrom(start_idx):
|
80 |
+
for i in range(start_idx, -1, -1):
|
81 |
+
if doc_tokens[i] == eos_token:
|
82 |
+
return i
|
83 |
+
return -1
|
84 |
+
|
85 |
+
chunks = []
|
86 |
+
|
87 |
+
while len(doc_tokens) > max_size:
|
88 |
+
# Splits at the eos token
|
89 |
+
eos_idx = _findPreviousEOSFrom(max_size - 1)
|
90 |
+
|
91 |
+
if eos_idx == -1:
|
92 |
+
# The sentence is too long.
|
93 |
+
# Find the next bos in front of the current sentence (if exists) and truncate the current sentence.
|
94 |
+
next_bos_idx = _findNextBOSFrom(max_size)
|
95 |
+
if next_bos_idx != -1:
|
96 |
+
doc_tokens = doc_tokens[:max_size-1] + [eos_token] + doc_tokens[next_bos_idx:]
|
97 |
+
else:
|
98 |
+
doc_tokens = doc_tokens[:max_size-1] + [eos_token]
|
99 |
+
eos_idx = max_size - 1
|
100 |
+
|
101 |
+
chunks.append(doc_tokens[:eos_idx+1])
|
102 |
+
doc_tokens = doc_tokens[eos_idx+1:]
|
103 |
+
|
104 |
+
if len(doc_tokens) > 0: chunks.append(doc_tokens) # Remaining part of the document
|
105 |
+
|
106 |
+
return chunks
|
tokenizer_config.json
ADDED
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"clean_up_tokenization_spaces": true,
|
3 |
+
"cls_token": "[CLS]",
|
4 |
+
"do_basic_tokenize": true,
|
5 |
+
"do_lower_case": true,
|
6 |
+
"mask_token": "[MASK]",
|
7 |
+
"model_max_length": 1000000000000000019884624838656,
|
8 |
+
"never_split": null,
|
9 |
+
"pad_token": "[PAD]",
|
10 |
+
"sep_token": "[SEP]",
|
11 |
+
"strip_accents": null,
|
12 |
+
"tokenize_chinese_chars": true,
|
13 |
+
"tokenizer_class": "BertTokenizer",
|
14 |
+
"unk_token": "[UNK]"
|
15 |
+
}
|
transformerutils.py
ADDED
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import torch
|
2 |
+
import torch.nn as nn
|
3 |
+
import math
|
4 |
+
|
5 |
+
|
6 |
+
|
7 |
+
# Source: https://pytorch.org/tutorials/beginner/transformer_tutorial.html
|
8 |
+
class PositionalEncoding(nn.Module):
|
9 |
+
def __init__(self, d_model, dropout=0.1, max_len=5000):
|
10 |
+
super().__init__()
|
11 |
+
self.dropout = nn.Dropout(p=dropout)
|
12 |
+
|
13 |
+
position = torch.arange(max_len).unsqueeze(1)
|
14 |
+
div_term = torch.exp(torch.arange(0, d_model, 2) * (-math.log(10000.0) / d_model))
|
15 |
+
pe = torch.zeros(max_len, d_model)
|
16 |
+
pe[:, 0::2] = torch.sin(position * div_term)
|
17 |
+
pe[:, 1::2] = torch.cos(position * div_term)
|
18 |
+
self.register_buffer("pe", pe)
|
19 |
+
|
20 |
+
def forward(self, x):
|
21 |
+
x = x + self.pe[:x.size(0)]
|
22 |
+
return self.dropout(x)
|
23 |
+
|
24 |
+
|
25 |
+
"""
|
26 |
+
Same scheduler as in "Attention Is All You Need"
|
27 |
+
"""
|
28 |
+
class NoamScheduler():
|
29 |
+
def __init__(self, optimizer, warmup, model_size):
|
30 |
+
self.epoch = 0
|
31 |
+
self.optimizer = optimizer
|
32 |
+
self.warmup = warmup
|
33 |
+
self.model_size = model_size
|
34 |
+
|
35 |
+
def step(self):
|
36 |
+
self.epoch += 1
|
37 |
+
new_lr = self.model_size**(-0.5) * min(self.epoch**(-0.5), self.epoch * self.warmup**(-1.5))
|
38 |
+
|
39 |
+
for param in self.optimizer.param_groups:
|
40 |
+
param["lr"] = new_lr
|
41 |
+
|
42 |
+
|
43 |
+
"""
|
44 |
+
Encoders to attend sentence level features.
|
45 |
+
"""
|
46 |
+
class TransformerInterEncoder(nn.Module):
|
47 |
+
def __init__(self, d_model, d_ff=2048, nheads=8, num_encoders=2, dropout=0.1, max_len=512):
|
48 |
+
super().__init__()
|
49 |
+
self.positional_enc = PositionalEncoding(d_model, dropout, max_len)
|
50 |
+
self.encoders = nn.TransformerEncoder(
|
51 |
+
nn.TransformerEncoderLayer(d_model=d_model, nhead=nheads, dim_feedforward=d_ff),
|
52 |
+
num_layers=num_encoders
|
53 |
+
)
|
54 |
+
self.layer_norm = nn.LayerNorm(d_model)
|
55 |
+
self.linear = nn.Linear(d_model, 1)
|
56 |
+
self.sigmoid = nn.Sigmoid()
|
57 |
+
|
58 |
+
def forward(self, x):
|
59 |
+
x = self.positional_enc(x)
|
60 |
+
x = self.encoders(x)
|
61 |
+
x = self.layer_norm(x)
|
62 |
+
logit = self.linear(x)
|
63 |
+
sentences_scores = self.sigmoid(logit)
|
64 |
+
|
65 |
+
return sentences_scores.squeeze(-1), logit.squeeze(-1)
|
utilities.py
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Pads a list to a given size
|
3 |
+
"""
|
4 |
+
def padToSize(to_pad_list, pad_size, filler):
|
5 |
+
return to_pad_list + [filler]*(pad_size-len(to_pad_list))
|
vocab.txt
ADDED
The diff for this file is too large to render.
See raw diff
|
|