├── README.md ├── ccf_2020_qa_match_concat.py ├── ccf_2020_qa_match_pair.py ├── ccf_2020_qa_match_pet.py ├── ccf_2020_qa_match_point.py ├── data ├── new_dict.txt ├── test │ ├── test.query.tsv │ └── test.reply.tsv └── train │ ├── train.query.tsv │ └── train.reply.tsv ├── img ├── bottom-embedding.png ├── concat.png ├── pair.png ├── pet.png ├── point.png ├── post-training.png ├── sc-loss.png ├── sc.png ├── ssc-loss.png ├── ssc.png ├── summary.png └── top-embedding.png ├── new_words_mining.py ├── pair-adversarial-train.py ├── pair-data-augment-contrastive-learning.py ├── pair-external-embedding.py ├── pair-post-training-wwm-sop.py ├── pair-self-kd.py ├── pair-supervised-contrastive-learning.py ├── point-post-training-wwm-sop.py ├── requirements-post-training.txt └── requirements.txt /README.md: -------------------------------------------------------------------------------- 1 | # Update 2 | 基于当前repo 优化后,A/B 榜皆是Top1,~~代码整理中,后续会陆续放上来!~~ 3 | 4 | 总结博客:[ccf问答匹配比赛(下):如何只用“bert”夺冠](https://xv44586.github.io/2021/01/20/ccf-qa-2/) 5 | 6 | # 优化思路 7 | ## Post training 8 | ### mlm 9 | 提升mlm任务中的mask策略,提升难度,提高下游性能:挖掘新词,加入词典,whole word mask + dynamic mask 10 | * 挖掘新词 11 | ```bash 12 | python new_words_mining.py 13 | ``` 14 | ### nsp 15 | 句子级别的任务是有用的,不过替换为SOP/AOP: query-answer pair时互换位置(sop),query-answer-list时,只打乱answer-list的顺序(aop) 16 | 17 | ### model-adaptive 18 | post training的样本格式与下游一致,也能带来提升(区别RoBERTa 中的结论) 19 | 20 | 完整post training代码为两份:query-answer pair 与 query-answerA-list两种方式: 21 | ```bash 22 | python popint-post-training-wwm-sop.py 23 | python pair-post-training-wwm-sop.py 24 | ``` 25 | 26 | PS: post training 后,bert 后接复杂分类层(CNN/RNN/DGCNN/...)基本不会带来提升 27 | ![post training result](./img/post-training.png) 28 | 29 | ## 融入知识 30 | 融入知识主要两种方式:bert 的Embedding层融入与transformer output层融入: 31 | * embedding层融合 32 | ![external-embedding-bottom](./img/bottom-embedding.png) 33 | * transformer output 层融合 34 | ![top-embedding](./img/top-embedding.png) 35 | 36 | 融入的知识使用的gensim 训练的word2vec(dims=100),不过两种方式多次实验后都没带来提升: 37 | ```bash 38 | python pair-external-embedding.py 39 | ``` 40 | 如何切换融入的方式,请查看代码后自行修改 41 | 42 | ## 对比学习 43 | 引入对比学习尝试提高模型性能,对比学习主要有两种方式:自监督对比学习与监督对比学习: 44 | * 自监督对比学习 45 | 通过互换QA位置,并随机mask 10%的token来构建一对view,view之间互为正例: 46 | * loss 47 | ![自监督对比学习loss](./img/ssc-loss.png) 48 | * model 49 | ![自监督对比学习模型](./img/ssc.png) 50 | 51 | * 监督对比学习 52 | 将相同label的样本视为互为正例: 53 | * loss 54 | ![监督对比学习loss](./img/sc-loss.png) 55 | * model 56 | ![监督对比学习模型](./img/sc.png) 57 | 58 | 执行自监督对比代码: 59 | ```bash 60 | python pair-data-augment-contrstive-learning.py 61 | ``` 62 | 执行监督对比学习代码: 63 | ```bash 64 | python pair-supervised-contrastive-learning.py 65 | ``` 66 | 67 | ## 自蒸馏 68 | 自蒸馏即Teacher 与 Student 为同一个模型,Teacher训练一次后,在train data上打上soften labels,然后迁移至Student 模型。 69 | ```bash 70 | python pair-self-kd.py 71 | ``` 72 | 73 | ## 对抗训练 74 | 使用FGM方法对EMbedding进行扰动: 75 | ```bash 76 | python pair-adversarial-train.py 77 | ``` 78 | 79 | ## 数据增强 80 | 数据增强主要尝试了两种方式:EDA与伪标签。 81 | * EDA 82 | 随机删除/随机替换/随机插入/随机重复,操作比例10%,每个样本生成4个新样本 83 | 词向量质量低,所以使用从当前句子随机选取一个词作为同义词进行操作 84 | 85 | * 伪标签 86 | 用已训练的模型对test data打上标签加入训练集 87 | 88 | Tips: 89 | 数据增强时用已训练模型进行过滤,将低置信度(<0.7)的样本过滤掉,避免引入错误标签样本;此外,伪标签时,要结合数据比例,过多的测试数据提前进入训练集,最终的结果只会与“伪标签”一致,反而无法带来提升。 90 | 91 | ## shuffle 92 | 在query-answer-list 样本格式下,解码时对answer-list进行全排列,然后投票。不过此次比赛的数据顺序很重要,乱序后结果较差,没带来提升 93 | 94 | 95 | # 总结 96 | ![](./img/summary.png) 97 | 98 | **-----------------------------------2020.01.18------------------------------------------------------------------** 99 | 100 | # 比赛 101 | 贝壳找房-房产行业聊天问答匹配, 比赛地址[https://www.datafountain.cn/competitions/474/datasets](https://www.datafountain.cn/competitions/474/datasets) 102 | 103 | 总结博客:[ccf问答匹配](https://xv44586.github.io/2020/11/08/ccf-qa/) 104 | 105 | # 简单说明 106 | 样本为一个问题多个回答,其中回答有的是针对问题的回答(1),也有不是的(0),其中回答是按顺序排列的。即: 107 | query1: [(answer1, 0), (answer2, 1),...] 108 | 任务是对每个回答进行分类,判断是不是针对问题的回答。 109 | 110 | # pretrain model weights 111 | 预训练模型使用的是华为开源的[nezha-base-wwm](https://github.com/huawei-noah/Pretrained-Language-Model/tree/master/NEZHA-TensorFlow) 112 | 113 | # Baseline 114 | ## 思路一: 115 | 不考虑回答之间的顺序关系,将其拆为query-answer 对,然后进行判断。 116 | 比如现在的样本是: {query: "房子几年了", answers: [("二年了", 1), ("楼层靠中间", 0)]},此时我们将其拆分为单个query-answer pair,即: 117 | [{query: "房子几年了", answer: "二年了", label: 1}, {query: "房子几年了", answer: "楼层靠中间", label: 0}] 118 | 119 | ![pair match](./img/pair.png) 120 | 121 | 代码实现:[pair_match](https://github.com/xv44586/ccf_2020_qa_match/ccf_2020_qa_match_pair.py) 122 | 123 | 单模型提交f1: 0.752 124 | 125 | ## 思路二: 126 | 考虑对话连贯性,同时考虑其完整性,将所有回答顺序拼接后再与问题拼接,组成query-answer1-answer2,然后针对每句回答进行分类。 127 | 上面的例子将被组成样本:{query: "房子几年了", answer: "两年了[SEP]楼层靠中间[SEP]", label: [mask, mask, mask, 0, mask, mask, mask,mask,mask, 0]} 128 | 即:将每句回答后面的[SEP] 作为最终的特征向量,然后去做二分类。 129 | 130 | ![](./img/point.png) 131 | 132 | 代码实现:[match_point](https://github.com/xv44586/ccf_2020_qa_match/ccf_2020_qa_match_point.py) 133 | 134 | 单模型提交f1: 0.75 135 | 136 | ## 思路三: 137 | Pattern-Exploiting Training(PET),即增加一个pattern,将任务转换为MLM任务,然后通过pattern的得分来判断对应的类别。 138 | 如本次样本可以添加一个前缀pattern:"简接回答问题"/"直接回答问题",分别对应label 0/1,pattern的得分只需看第一个位置中"间"/"直" 两个token的概率谁高即可。 139 | 此外,训练时还可以借助bert的预训练任务中的mlm任务增强模型的泛化能力。更详细的请介绍请查阅[文本分类秒解](https://xv44586.github.io/2020/10/25/pet/) 140 | 141 | 对于本次样本,对应的示意图如下: 142 | 143 | ![](./img/pet.png) 144 | 145 | 对应代码实现:[pet classification](https://github.com/xv44586/ccf_2020_qa_match/ccf_2020_qa_match_pet.py) 146 | 147 | 单模型提交f1: 0.76+ 148 | 149 | # 思路四 150 | 由于bert 不同的transformer 层提取到的语义粒度不同,而不同粒度的信息对分类来说起到的作用也不同,所以可以concat所以粒度的语义信息,拼接后作为特征进行分类。 151 | 152 | 对应于本次样本,示意图如下: 153 | ![](./img/concat.png) 154 | 155 | 对应代码实现:[concat classification](https://github.com/xv44586/ccf_2020_qa_match/ccf_2020_qa_match_concat.py) 156 | 单模型提交f1: 0.75+ 157 | 158 | # tips 159 | 贴几篇感觉有启发的关于文本分类的论文 160 | 161 | * [Universal Language Model Fine-tuning for Text Classification](http://arxiv.org/abs/1801.06146) 162 | * [How to Fine-Tune BERT for Text Classification?](http://arxiv.org/abs/1905.05583) 163 | * [Don't Stop Pretraining: Adapt Language Models to Domains and Tasks](http://arxiv.org/abs/2004.10964) 164 | * [Enriching BERT with Knowledge Graph Embeddings for Document Classification](http://arxiv.org/abs/1909.08402) 165 | * [Hate Speech Detection and Racial Bias Mitigation in Social Media based on BERT model](https://arxiv.org/pdf/2008.06460.pdf) 166 | * [Contrastive Self-Supervised Learning](http://ankeshanand.com/blog/2020/01/26/contrative-self-supervised-learning.html) 167 | * [A Survey on Contrastive Self-supervised Learning](http://arxiv.org/abs/2011.00362) 168 | * [Supervised Contrastive Learning for Pre-trained Language Model Fine-tuning](http://arxiv.org/abs/2011.01403) 169 | * [Self-Attention with Relative Position Representations](http://arxiv.org/abs/1803.02155) 170 | * [RoBERTa: A Robustly Optimized BERT Pretraining Approach](http://arxiv.org/abs/1907.11692) 171 | * [NEZHA: Neural Contextualized Representation for Chinese Language Understanding](http://arxiv.org/abs/1909.00204) 172 | * [对抗训练浅谈:意义、方法和思考(附Keras实现)](https://kexue.fm/archives/7234) 173 | * [Train No Evil: Selective Masking for Task-Guided Pre-Training](https://arxiv.org/abs/2004.09733) -------------------------------------------------------------------------------- /ccf_2020_qa_match_concat.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf8 -*- 3 | # @Date : 2020/11/12 4 | # @Author : mingming.xu 5 | # @Email : xv44586@gmail.com 6 | """ 7 | bert每层捕获的信息不同,代表的语义粒度也不同,将不同粒度的信息拼接起来,然后送进CNN后做分类。 8 | ret: 9 | https://arxiv.org/pdf/2008.06460.pdf 10 | """ 11 | 12 | import os 13 | from tqdm import tqdm 14 | import numpy as np 15 | 16 | from toolkit4nlp.utils import * 17 | from toolkit4nlp.models import * 18 | from toolkit4nlp.layers import * 19 | from toolkit4nlp.optimizers import * 20 | from toolkit4nlp.tokenizers import Tokenizer 21 | from toolkit4nlp.backend import * 22 | 23 | batch_size = 16 24 | maxlen = 280 25 | epochs = 10 26 | lr = 1e-5 27 | 28 | # bert配置 29 | config_path = '/home/mingming.xu/pretrain/NLP/nezha_base_wwm/bert_config.json' 30 | checkpoint_path = '/home/mingming.xu/pretrain/NLP/nezha_base_wwm/model.ckpt' 31 | dict_path = '/home/mingming.xu/pretrain/NLP/nezha_base_wwm//vocab.txt' 32 | # 建立分词器 33 | tokenizer = Tokenizer(dict_path, do_lower_case=True) 34 | 35 | path = '/home/mingming.xu/datasets/NLP/ccf_qa_match/' 36 | 37 | 38 | def load_data(train_test='train'): 39 | D = {} 40 | with open(os.path.join(path, train_test, train_test + '.query.tsv')) as f: 41 | for l in f: 42 | span = l.strip().split('\t') 43 | D[span[0]] = {'query': span[1], 'reply': []} 44 | 45 | with open(os.path.join(path, train_test, train_test + '.reply.tsv')) as f: 46 | for l in f: 47 | span = l.strip().split('\t') 48 | if len(span) == 4: 49 | q_id, r_id, r, label = span 50 | else: 51 | label = None 52 | q_id, r_id, r = span 53 | D[q_id]['reply'].append([r_id, r, label]) 54 | d = [] 55 | for k, v in D.items(): 56 | q_id = k 57 | q = v['query'] 58 | reply = v['reply'] 59 | 60 | for r in reply: 61 | r_id, rc, label = r 62 | 63 | d.append([q_id, q, r_id, rc, label]) 64 | return d 65 | 66 | 67 | train_data = load_data('train') 68 | test_data = load_data('test') 69 | 70 | 71 | class data_generator(DataGenerator): 72 | def __iter__(self, shuffle=False): 73 | batch_token_ids, batch_segment_ids, batch_labels = [], [], [] 74 | for is_end, (q_id, q, r_id, r, label) in self.get_sample(shuffle): 75 | label = int(label) if label is not None else None 76 | 77 | token_ids, segment_ids = tokenizer.encode(q, r, maxlen=256) 78 | 79 | batch_token_ids.append(token_ids) 80 | batch_segment_ids.append(segment_ids) 81 | batch_labels.append([label]) 82 | 83 | if is_end or len(batch_token_ids) == self.batch_size: 84 | batch_token_ids = pad_sequences(batch_token_ids) 85 | batch_segment_ids = pad_sequences(batch_segment_ids) 86 | batch_labels = pad_sequences(batch_labels) 87 | 88 | yield [batch_token_ids, batch_segment_ids], batch_labels 89 | 90 | batch_token_ids, batch_segment_ids, batch_labels = [], [], [] 91 | 92 | 93 | # shuffle 94 | np.random.shuffle(train_data) 95 | n = int(len(train_data) * 0.8) 96 | train_generator = data_generator(train_data[:n], batch_size) 97 | valid_generator = data_generator(train_data[n:], batch_size) 98 | test_generator = data_generator(test_data, batch_size) 99 | 100 | # 加载预训练模型 101 | bert = build_transformer_model( 102 | config_path=config_path, 103 | checkpoint_path=checkpoint_path, 104 | # model='bert', # 加载bert/Roberta/ernie 105 | # model='electra', # 加载electra 106 | model='nezha', # 加载NEZHA 107 | return_keras_model=False, 108 | ) 109 | inputs = bert.inputs 110 | 111 | outputs = [] 112 | x = bert.apply_embeddings(inputs) 113 | 114 | for idx in range(bert.num_hidden_layers): 115 | x = bert.apply_transformer_layers(x, idx) 116 | output = Lambda(lambda x: x[:, 0:1])(x) 117 | outputs.append(output) 118 | 119 | output = Concatenate(1)(outputs) 120 | 121 | output = DGCNN(dilation_rate=1, dropout_rate=0.1)(output) 122 | output = DGCNN(dilation_rate=2, dropout_rate=0.1)(output) 123 | output = DGCNN(dilation_rate=2, dropout_rate=0.1)(output) 124 | output = DGCNN(dilation_rate=1, dropout_rate=0.1)(output) 125 | 126 | output = AttentionPooling1D()(output) 127 | output = Dropout(0.5)(output) 128 | output = Dense(1, activation='sigmoid')(output) 129 | 130 | model = keras.models.Model(inputs, output) 131 | model.summary() 132 | 133 | model.compile( 134 | # loss=binary_focal_loss(0.25, 12), # focal loss 135 | loss=K.binary_crossentropy, 136 | optimizer=Adam(2e-5), 137 | metrics=['accuracy'], 138 | ) 139 | 140 | 141 | def evaluate(data): 142 | P, R, TP = 0., 0., 0. 143 | for x_true, y_true in tqdm(data): 144 | y_pred = model.predict(x_true)[:, 0] 145 | y_pred = np.round(y_pred) 146 | y_true = y_true[:, 0] 147 | 148 | R += y_pred.sum() 149 | P += y_true.sum() 150 | TP += ((y_pred + y_true) > 1).sum() 151 | 152 | print(P, R, TP) 153 | pre = TP / R 154 | rec = TP / P 155 | 156 | return 2 * (pre * rec) / (pre + rec) 157 | 158 | 159 | class Evaluator(keras.callbacks.Callback): 160 | """评估与保存 161 | """ 162 | 163 | def __init__(self): 164 | self.best_val_f1 = 0. 165 | 166 | def on_epoch_end(self, epoch, logs=None): 167 | val_f1 = evaluate(valid_generator) 168 | if val_f1 > self.best_val_f1: 169 | self.best_val_f1 = val_f1 170 | model.save_weights('best_concat_model.weights') 171 | print( 172 | u'val_f1: %.5f, best_val_f1: %.5f\n' % 173 | (val_f1, self.best_val_f1) 174 | ) 175 | 176 | 177 | def predict_to_file(path='concat_submission.tsv', data=test_generator): 178 | preds = [] 179 | for x, _ in tqdm(test_generator): 180 | pred = model.predict(x)[:, 0] 181 | pred = np.round(pred) 182 | pred = pred.astype(int) 183 | preds.extend(pred) 184 | 185 | ret = [] 186 | for d, p in zip(test_data, preds): 187 | q_id, _, r_id, _, _ = d 188 | ret.append([str(q_id), str(r_id), str(p)]) 189 | 190 | with open(path, 'w', encoding='utf8') as f: 191 | for l in ret: 192 | f.write('\t'.join(l) + '\n') 193 | 194 | 195 | if __name__ == '__main__': 196 | evaluator = Evaluator() 197 | model.fit_generator( 198 | train_generator.generator(), 199 | steps_per_epoch=len(train_generator), 200 | epochs=5, 201 | callbacks=[evaluator], 202 | ) 203 | 204 | # predict test and write to file 205 | model.load_weights('best_concat_model.weights') 206 | predict_to_file() 207 | -------------------------------------------------------------------------------- /ccf_2020_qa_match_pair.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Date : 2020/11/3 3 | # @Author : mingming.xu 4 | # @Email : xv44586@gmail.com 5 | # @File : ccf_2020_qa_match_pair.py 6 | """ 7 | 拆成query-pair 对,然后分类 8 | 线上f1:0.752 9 | 10 | tips: 11 | 切换模型时,修改对应config_path/checkpoint_path/dict_path路径以及build_transformer_model 内的参数 12 | """ 13 | import os 14 | from tqdm import tqdm 15 | import numpy as np 16 | 17 | from toolkit4nlp.utils import * 18 | from toolkit4nlp.models import * 19 | from toolkit4nlp.layers import * 20 | from toolkit4nlp.optimizers import * 21 | from toolkit4nlp.tokenizers import Tokenizer 22 | from toolkit4nlp.backend import * 23 | 24 | batch_size = 16 25 | maxlen = 280 26 | epochs = 10 27 | lr = 1e-5 28 | 29 | # bert配置 30 | config_path = '/home/mingming.xu/pretrain/NLP/nezha_base_wwm/bert_config.json' 31 | checkpoint_path = '/home/mingming.xu/pretrain/NLP/nezha_base_wwm/model.ckpt' 32 | dict_path = '/home/mingming.xu/pretrain/NLP/nezha_base_wwm//vocab.txt' 33 | # 建立分词器 34 | tokenizer = Tokenizer(dict_path, do_lower_case=True) 35 | 36 | path = '/home/mingming.xu/datasets/NLP/ccf_qa_match/' 37 | 38 | 39 | def load_data(train_test='train'): 40 | D = {} 41 | with open(os.path.join(path, train_test, train_test + '.query.tsv')) as f: 42 | for l in f: 43 | span = l.strip().split('\t') 44 | D[span[0]] = {'query': span[1], 'reply': []} 45 | 46 | with open(os.path.join(path, train_test, train_test + '.reply.tsv')) as f: 47 | for l in f: 48 | span = l.strip().split('\t') 49 | if len(span) == 4: 50 | q_id, r_id, r, label = span 51 | else: 52 | label = None 53 | q_id, r_id, r = span 54 | D[q_id]['reply'].append([r_id, r, label]) 55 | d = [] 56 | for k, v in D.items(): 57 | q_id = k 58 | q = v['query'] 59 | reply = v['reply'] 60 | 61 | for r in reply: 62 | r_id, rc, label = r 63 | 64 | d.append([q_id, q, r_id, rc, label]) 65 | return d 66 | 67 | 68 | train_data = load_data('train') 69 | test_data = load_data('test') 70 | 71 | 72 | class data_generator(DataGenerator): 73 | def __iter__(self, shuffle=False): 74 | batch_token_ids, batch_segment_ids, batch_labels = [], [], [] 75 | for is_end, (q_id, q, r_id, r, label) in self.get_sample(shuffle): 76 | label = int(label) if label is not None else None 77 | 78 | token_ids, segment_ids = tokenizer.encode(q, r, maxlen=256) 79 | 80 | batch_token_ids.append(token_ids) 81 | batch_segment_ids.append(segment_ids) 82 | batch_labels.append([label]) 83 | 84 | if is_end or len(batch_token_ids) == self.batch_size: 85 | batch_token_ids = pad_sequences(batch_token_ids) 86 | batch_segment_ids = pad_sequences(batch_segment_ids) 87 | batch_labels = pad_sequences(batch_labels) 88 | 89 | yield [batch_token_ids, batch_segment_ids], batch_labels 90 | 91 | batch_token_ids, batch_segment_ids, batch_labels = [], [], [] 92 | 93 | 94 | # shuffle 95 | np.random.shuffle(train_data) 96 | n = int(len(train_data) * 0.8) 97 | train_generator = data_generator(train_data[:n], batch_size) 98 | valid_generator = data_generator(train_data[n:], batch_size) 99 | test_generator = data_generator(test_data, batch_size) 100 | 101 | # 加载预训练模型 102 | bert = build_transformer_model( 103 | config_path=config_path, 104 | checkpoint_path=checkpoint_path, 105 | # model='bert', # 加载bert/Roberta/ernie 106 | # model='electra', # 加载electra 107 | model='nezha', # 加载NEZHA 108 | ) 109 | output = bert.output 110 | 111 | output = Dropout(0.5)(output) 112 | 113 | att = AttentionPooling1D(name='attention_pooling_1')(output) 114 | 115 | output = ConcatSeq2Vec()([output, att]) 116 | output = DGCNN(dilation_rate=1, dropout_rate=0.1)(output) 117 | output = DGCNN(dilation_rate=2, dropout_rate=0.1)(output) 118 | output = DGCNN(dilation_rate=5, dropout_rate=0.1)(output) 119 | output = Lambda(lambda x: x[:, 0])(output) 120 | output = Dense(1, activation='sigmoid')(output) 121 | 122 | model = keras.models.Model(bert.input, output) 123 | model.summary() 124 | 125 | 126 | model.compile( 127 | loss=K.binary_crossentropy, 128 | optimizer=Adam(2e-5), # 用足够小的学习率 129 | metrics=['accuracy'], 130 | ) 131 | 132 | 133 | def evaluate(data): 134 | P, R, TP = 0., 0., 0. 135 | for x_true, y_true in tqdm(data): 136 | y_pred = model.predict(x_true)[:, 0] 137 | y_pred = np.round(y_pred) 138 | y_true = y_true[:, 0] 139 | 140 | R += y_pred.sum() 141 | P += y_true.sum() 142 | TP += ((y_pred + y_true) > 1).sum() 143 | 144 | print(P, R, TP) 145 | pre = TP / R 146 | rec = TP / P 147 | 148 | return 2 * (pre * rec) / (pre + rec) 149 | 150 | 151 | class Evaluator(keras.callbacks.Callback): 152 | """评估与保存 153 | """ 154 | 155 | def __init__(self): 156 | self.best_val_f1 = 0. 157 | 158 | def on_epoch_end(self, epoch, logs=None): 159 | val_f1 = evaluate(valid_generator) 160 | if val_f1 > self.best_val_f1: 161 | self.best_val_f1 = val_f1 162 | model.save_weights('best_parimatch_model.weights') 163 | print( 164 | u'val_f1: %.5f, best_val_f1: %.5f\n' % 165 | (val_f1, self.best_val_f1) 166 | ) 167 | 168 | 169 | def predict_to_file(path='pair_submission.tsv', data=test_generator): 170 | preds = [] 171 | for x, _ in tqdm(test_generator): 172 | pred = model.predict(x)[:, 0] 173 | pred = np.round(pred) 174 | pred = pred.astype(int) 175 | preds.extend(pred) 176 | 177 | ret = [] 178 | for d, p in zip(test_data, preds): 179 | q_id, _, r_id, _, _ = d 180 | ret.append([str(q_id), str(r_id), str(p)]) 181 | 182 | with open(path, 'w', encoding='utf8') as f: 183 | for l in ret: 184 | f.write('\t'.join(l) + '\n') 185 | 186 | 187 | if __name__ == '__main__': 188 | evaluator = Evaluator() 189 | model.fit_generator( 190 | train_generator.generator(), 191 | steps_per_epoch=len(train_generator), 192 | epochs=5, 193 | callbacks=[evaluator], 194 | ) 195 | 196 | # predict test and write to file 197 | model.load_weights('best_parimatch_model.weights') 198 | predict_to_file() 199 | -------------------------------------------------------------------------------- /ccf_2020_qa_match_pet.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Date : 2020/11/4 3 | # @Author : mingming.xu 4 | # @Email : xv44586@gmail.com 5 | # @File : ccf_2020_qa_match_pet.py 6 | """ 7 | Pattern-Exploiting Training(PET): 增加pattern,将任务转换为MLM任务。 8 | 线上f1: 0.761 9 | 10 | tips: 11 | 切换模型时,修改对应config_path/checkpoint_path/dict_path路径以及build_transformer_model 内的参数 12 | """ 13 | 14 | import os 15 | import numpy as np 16 | import json 17 | from tqdm import tqdm 18 | import numpy as np 19 | 20 | from toolkit4nlp.backend import keras, K 21 | from toolkit4nlp.tokenizers import Tokenizer, load_vocab 22 | from toolkit4nlp.models import build_transformer_model, Model 23 | from toolkit4nlp.optimizers import * 24 | from toolkit4nlp.utils import pad_sequences, DataGenerator 25 | from toolkit4nlp.layers import * 26 | 27 | path = '/home/mingming.xu/datasets/NLP/ccf_qa_match/' 28 | 29 | p = os.path.join(path, 'train', 'train.query.tsv') 30 | 31 | 32 | def load_data(train_test='train'): 33 | D = {} 34 | with open(os.path.join(path, train_test, train_test + '.query.tsv')) as f: 35 | for l in f: 36 | span = l.strip().split('\t') 37 | D[span[0]] = {'query': span[1], 'reply': []} 38 | 39 | with open(os.path.join(path, train_test, train_test + '.reply.tsv')) as f: 40 | for l in f: 41 | span = l.strip().split('\t') 42 | if len(span) == 4: 43 | q_id, r_id, r, label = span 44 | label = int(label) 45 | else: 46 | label = None 47 | q_id, r_id, r = span 48 | D[q_id]['reply'].append([r_id, r, label]) 49 | d = [] 50 | for k, v in D.items(): 51 | q_id = k 52 | q = v['query'] 53 | reply = v['reply'] 54 | 55 | for i, r in enumerate(reply): 56 | r_id, rc, label = r 57 | d.append([q_id, q, r_id, rc, label]) 58 | return d 59 | 60 | 61 | train_data = load_data('train') 62 | test_data = load_data('test') 63 | 64 | num_classes = 32 65 | maxlen = 128 66 | batch_size = 8 67 | 68 | # BERT base 69 | 70 | config_path = '/home/mingming.xu/pretrain/NLP/nezha_base_wwm/bert_config.json' 71 | checkpoint_path = '/home/mingming.xu/pretrain/NLP/nezha_base_wwm/model.ckpt' 72 | dict_path = '/home/mingming.xu/pretrain/NLP/nezha_base_wwm/vocab.txt' 73 | 74 | # tokenizer 75 | tokenizer = Tokenizer(dict_path, do_lower_case=True) 76 | 77 | # pattern 78 | pattern = '直接回答问题:' 79 | mask_idx = [1] 80 | 81 | id2label = { 82 | 0: '间', 83 | 1: '直' 84 | } 85 | 86 | label2id = {v: k for k, v in id2label.items()} 87 | labels = list(id2label.values()) 88 | 89 | 90 | def random_masking(token_ids): 91 | """对输入进行随机mask 92 | """ 93 | rands = np.random.random(len(token_ids)) 94 | source, target = [], [] 95 | for r, t in zip(rands, token_ids): 96 | if r < 0.15 * 0.8: 97 | source.append(tokenizer._token_mask_id) 98 | target.append(t) 99 | elif r < 0.15 * 0.9: 100 | source.append(t) 101 | target.append(t) 102 | elif r < 0.15: 103 | source.append(np.random.choice(tokenizer._vocab_size - 1) + 1) 104 | target.append(t) 105 | else: 106 | source.append(t) 107 | target.append(0) 108 | return source, target 109 | 110 | 111 | class data_generator(DataGenerator): 112 | def __init__(self, prefix=False, *args, **kwargs): 113 | super(data_generator, self).__init__(*args, **kwargs) 114 | self.prefix = prefix 115 | 116 | def __iter__(self, shuffle=False): 117 | batch_token_ids, batch_segment_ids, batch_target_ids = [], [], [] 118 | 119 | for is_end, (q_id, q, r_id, r, label) in self.get_sample(shuffle): 120 | label = int(label) if label is not None else None 121 | 122 | if label is not None or self.prefix: 123 | q = pattern + q 124 | 125 | token_ids, segment_ids = tokenizer.encode(q, r, maxlen=maxlen) 126 | 127 | if shuffle: 128 | source_tokens, target_tokens = random_masking(token_ids) 129 | else: 130 | source_tokens, target_tokens = token_ids[:], token_ids[:] 131 | 132 | # mask label 133 | if label is not None: 134 | label_ids = tokenizer.encode(id2label[label])[0][1:-1] 135 | for m, lb in zip(mask_idx, label_ids): 136 | source_tokens[m] = tokenizer._token_mask_id 137 | target_tokens[m] = lb 138 | elif self.prefix: 139 | for i in mask_idx: 140 | source_tokens[i] = tokenizer._token_mask_id 141 | 142 | batch_token_ids.append(source_tokens) 143 | batch_segment_ids.append(segment_ids) 144 | batch_target_ids.append(target_tokens) 145 | 146 | if is_end or len(batch_token_ids) == self.batch_size: 147 | batch_token_ids = pad_sequences(batch_token_ids) 148 | batch_segment_ids = pad_sequences(batch_segment_ids) 149 | batch_target_ids = pad_sequences(batch_target_ids) 150 | 151 | yield [batch_token_ids, batch_segment_ids, batch_target_ids], None 152 | 153 | batch_token_ids, batch_segment_ids, batch_target_ids = [], [], [] 154 | 155 | 156 | # shuffle 157 | np.random.shuffle(train_data) 158 | n = int(len(train_data) * 0.8) 159 | train_generator = data_generator(data=train_data[: n] + test_data, batch_size=batch_size) 160 | valid_generator = data_generator(data=train_data[n:], batch_size=batch_size) 161 | test_generator = data_generator(data=test_data, batch_size=batch_size, prefix=True) 162 | 163 | 164 | class CrossEntropy(Loss): 165 | """交叉熵作为loss,并mask掉输入部分 166 | """ 167 | 168 | def compute_loss(self, inputs, mask=None): 169 | y_true, y_pred = inputs 170 | y_mask = K.cast(K.not_equal(y_true, 0), K.floatx()) 171 | accuracy = keras.metrics.sparse_categorical_accuracy(y_true, y_pred) 172 | accuracy = K.sum(accuracy * y_mask) / K.sum(y_mask) 173 | self.add_metric(accuracy, name='accuracy') 174 | loss = K.sparse_categorical_crossentropy(y_true, y_pred) 175 | loss = K.sum(loss * y_mask) / K.sum(y_mask) 176 | return loss 177 | 178 | 179 | model = build_transformer_model(config_path=config_path, 180 | checkpoint_path=checkpoint_path, 181 | with_mlm=True, 182 | # model='bert', # 加载bert/Roberta/ernie 183 | model='nezha' 184 | ) 185 | 186 | target_in = Input(shape=(None,)) 187 | output = CrossEntropy(1)([target_in, model.output]) 188 | 189 | train_model = Model(model.inputs + [target_in], output) 190 | 191 | AdamW = extend_with_weight_decay(Adam) 192 | AdamWG = extend_with_gradient_accumulation(AdamW) 193 | 194 | opt = AdamWG(learning_rate=1e-5, exclude_from_weight_decay=['Norm', 'bias'], grad_accum_steps=4) 195 | train_model.compile(opt) 196 | train_model.summary() 197 | 198 | label_ids = np.array([tokenizer.encode(l)[0][1:-1] for l in labels]) 199 | 200 | 201 | def predict(x): 202 | if len(x) == 3: 203 | x = x[:2] 204 | y_pred = model.predict(x)[:, mask_idx] 205 | y_pred = y_pred[:, 0, label_ids[:, 0]] 206 | y_pred = y_pred.argmax(axis=1) 207 | return y_pred 208 | 209 | 210 | def evaluate(data): 211 | P, R, TP = 0., 0., 0. 212 | for d, _ in tqdm(data): 213 | x_true, y_true = d[:2], d[2] 214 | 215 | y_pred = predict(x_true) 216 | y_true = np.array([labels.index(tokenizer.decode(y)) for y in y_true[:, mask_idx]]) 217 | # print(y_true, y_pred) 218 | R += y_pred.sum() 219 | P += y_true.sum() 220 | TP += ((y_pred + y_true) > 1).sum() 221 | 222 | print(P, R, TP) 223 | pre = TP / R 224 | rec = TP / P 225 | 226 | return 2 * (pre * rec) / (pre + rec) 227 | 228 | 229 | class Evaluator(keras.callbacks.Callback): 230 | def __init__(self): 231 | self.best_acc = 0. 232 | 233 | def on_epoch_end(self, epoch, logs=None): 234 | acc = evaluate(valid_generator) 235 | if acc > self.best_acc: 236 | self.best_acc = acc 237 | self.model.save_weights('best_pet_model.weights') 238 | print('acc :{}, best acc:{}'.format(acc, self.best_acc)) 239 | 240 | 241 | def write_to_file(path): 242 | preds = [] 243 | for x, _ in tqdm(test_generator): 244 | pred = predict(x) 245 | preds.extend(pred) 246 | 247 | ret = [] 248 | for data, p in zip(test_data, preds): 249 | ret.append([data[0], data[2], str(p)]) 250 | 251 | with open(path, 'w') as f: 252 | for r in ret: 253 | f.write('\t'.join(r) + '\n') 254 | 255 | 256 | if __name__ == '__main__': 257 | evaluator = Evaluator() 258 | train_model.fit_generator(train_generator.generator(), 259 | steps_per_epoch=len(train_generator), 260 | epochs=10, 261 | callbacks=[evaluator]) 262 | 263 | train_model.load_weights('best_pet_model.weights') 264 | write_to_file('submission.tsv') 265 | -------------------------------------------------------------------------------- /ccf_2020_qa_match_point.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Date : 2020/10/28 3 | # @Author : mingming.xu 4 | # @Email : xv44586@gmail.com 5 | # @File : ccf_2020_qa_match_point.py 6 | """ 7 | 比赛:[房产行业聊天问答匹配](https://www.datafountain.cn/competitions/474/) 8 | 主要思路:将reply顺序拼接到query后面,利用每个reply的[SEP]token做二分类 9 | best val f1: 0.794 10 | A 榜: 0.756 11 | 12 | tips: 13 | 切换模型时,修改对应config_path/checkpoint_path/dict_path路径以及build_transformer_model 内的参数 14 | """ 15 | import os 16 | from tqdm import tqdm 17 | import numpy as np 18 | 19 | from toolkit4nlp.utils import * 20 | from toolkit4nlp.models import * 21 | from toolkit4nlp.layers import * 22 | from toolkit4nlp.optimizers import * 23 | from toolkit4nlp.tokenizers import Tokenizer 24 | from toolkit4nlp.backend import * 25 | 26 | batch_size = 16 27 | maxlen = 280 28 | epochs = 10 29 | lr = 1e-5 30 | 31 | # bert配置 32 | config_path = '/home/mingming.xu/pretrain/NLP/nezha_base_wwm/bert_config.json' 33 | checkpoint_path = '/home/mingming.xu/pretrain/NLP/nezha_base_wwm/model.ckpt' 34 | dict_path = '/home/mingming.xu/pretrain/NLP/nezha_base_wwm//vocab.txt' 35 | # 建立分词器 36 | tokenizer = Tokenizer(dict_path, do_lower_case=True) 37 | 38 | path = '/home/mingming.xu/datasets/NLP/ccf_qa_match/' 39 | 40 | 41 | def load_data(train_test='train'): 42 | D = {} 43 | with open(os.path.join(path, train_test, train_test + '.query.tsv')) as f: 44 | for l in f: 45 | span = l.strip().split('\t') 46 | D[span[0]] = {'query': span[1], 'reply': []} 47 | 48 | with open(os.path.join(path, train_test, train_test + '.reply.tsv')) as f: 49 | for l in f: 50 | span = l.strip().split('\t') 51 | if len(span) == 4: 52 | q_id, r_id, r, label = span 53 | else: 54 | label = None 55 | q_id, r_id, r = span 56 | D[q_id]['reply'].append([r_id, r, label]) 57 | d = [] 58 | for k, v in D.items(): 59 | v.update({'query_id': k}) 60 | d.append(v) 61 | return d 62 | 63 | 64 | train_data = load_data('train') 65 | test_data = load_data('test') 66 | 67 | 68 | class data_generator(DataGenerator): 69 | def __iter__(self, shuffle=False): 70 | batch_token_ids, batch_segment_ids, batch_mask, batch_labels = [], [], [], [] 71 | for is_end, item in self.get_sample(shuffle): 72 | query = item['query'] 73 | reply = item['reply'] 74 | token_ids, segment_ids = tokenizer.encode(query) 75 | mask_ids, label_ids = segment_ids[:], segment_ids[:] 76 | for rp in reply: 77 | _, r, label = rp 78 | r_token_ids = tokenizer.encode(r)[0][1:] 79 | r_segment_ids = [1] * len(r_token_ids) 80 | r_mask_ids = [0] * (len(r_token_ids) - 1) + [1] # 每句的句尾sep作为特征 81 | r_label_ids = r_mask_ids[:] 82 | if label and int(label) == 0: 83 | r_label_ids[-1] = 0 84 | 85 | token_ids += r_token_ids 86 | segment_ids += r_segment_ids 87 | mask_ids += r_mask_ids 88 | label_ids += r_label_ids 89 | 90 | batch_token_ids.append(token_ids) 91 | batch_segment_ids.append(segment_ids) 92 | batch_mask.append(mask_ids) 93 | batch_labels.append(label_ids) 94 | 95 | if is_end or len(batch_token_ids) == self.batch_size: 96 | batch_token_ids = pad_sequences(batch_token_ids) 97 | batch_segment_ids = pad_sequences(batch_segment_ids) 98 | batch_mask = pad_sequences(batch_mask) 99 | batch_labels = pad_sequences(batch_labels) 100 | 101 | yield [batch_token_ids, batch_segment_ids, batch_labels, batch_mask], None 102 | 103 | batch_token_ids, batch_segment_ids, batch_mask, batch_labels = [], [], [], [] 104 | 105 | 106 | train_generator = data_generator(train_data[:5000], batch_size) 107 | valid_generator = data_generator(train_data[5000:], batch_size) 108 | test_generator = data_generator(test_data, batch_size) 109 | 110 | 111 | class PointLoss(Loss): 112 | def compute_loss(self, inputs, mask=None): 113 | y_pred, y_true, label_mask = inputs 114 | loss = K.binary_crossentropy(y_true, y_pred) 115 | loss = K.sum(loss * label_mask) / K.sum(label_mask) 116 | return loss 117 | 118 | 119 | class ClsMerge(Layer): 120 | def call(self, inputs): 121 | input_shape = K.shape(inputs) 122 | cls = inputs[:, 0] 123 | cls = K.expand_dims(cls, 1) 124 | cls = K.tile(cls, [1, input_shape[1], 1]) 125 | 126 | return K.concatenate([inputs, cls], axis=-1) 127 | 128 | def compute_output_shape(self, input_shape): 129 | return input_shape[:2] + (input_shape[2] * 2,) 130 | 131 | def compute_mask(self, inputs, mask=None): 132 | return mask 133 | 134 | 135 | class ConcatSeq2Vec(Layer): 136 | def __init__(self, **kwargs): 137 | super(ConcatSeq2Vec, self).__init__(**kwargs) 138 | 139 | def build(self, input_shape): 140 | super(ConcatSeq2Vec, self).build(input_shape) 141 | 142 | def call(self, x): 143 | seq, vec = x 144 | vec = K.expand_dims(vec, 1) 145 | vec = K.tile(vec, [1, K.shape(seq)[1], 1]) 146 | return K.concatenate([seq, vec], 2) 147 | 148 | def compute_mask(self, inputs, mask): 149 | return mask[0] 150 | 151 | def compute_output_shape(self, input_shape): 152 | return input_shape[0][:-1] + (input_shape[0][-1] + input_shape[1][-1],) 153 | 154 | 155 | bert = build_transformer_model( 156 | config_path=config_path, 157 | checkpoint_path=checkpoint_path, 158 | # model='bert', # 加载bert/Roberta/ernie 159 | # model='electra', # 加载electra 160 | model='nezha', 161 | ) 162 | 163 | m_inputs = Input(shape=(None,)) 164 | label_inputs = Input(shape=(None,)) 165 | output = bert.output 166 | 167 | output = Dropout(0.5)(output) 168 | output = Dense(384, activation='tanh')(output) 169 | att = AttentionPooling1D(name='attention_pooling_1')(output) 170 | output = ConcatSeq2Vec()([output, att]) 171 | 172 | output = DGCNN(dilation_rate=1, dropout_rate=0.1)(output) 173 | output = DGCNN(dilation_rate=2, dropout_rate=0.1)(output) 174 | output = DGCNN(dilation_rate=5, dropout_rate=0.1)(output) 175 | 176 | output = Dense(1, activation='sigmoid')(output) 177 | output = Lambda(lambda x: x[:, :, 0])(output) 178 | x = PointLoss(0)([output, label_inputs, m_inputs]) 179 | 180 | train_model = Model(bert.inputs + [label_inputs, m_inputs], x) 181 | 182 | infer_model = Model(bert.inputs, output) 183 | train_model.compile(optimizer=Adam(5e-5)) 184 | train_model.summary() 185 | 186 | 187 | def extract_label(labels, label_masks): 188 | """ 189 | 从label序列中提取出每个回复对应的label 190 | """ 191 | labels = labels[label_masks > 0] 192 | labels = list(labels) 193 | 194 | p = [] 195 | s, e = 0, 0 196 | for lm in label_masks: 197 | e += lm.sum() 198 | p.append(labels[s:e]) 199 | s = e 200 | return p 201 | 202 | 203 | def predict(item): 204 | ''' 205 | 获取对应回复的label 206 | ''' 207 | token_ids, segment_ids, label_mask = item 208 | pred = infer_model.predict([token_ids, segment_ids]) 209 | pred = np.round(pred) 210 | 211 | return extract_label(pred, label_mask) 212 | 213 | 214 | def evaluate(data=valid_generator): 215 | P, R, TP = 0., 0., 0. 216 | for (tokens, segments, labels, label_masks), _ in tqdm(data): 217 | y_pred = predict([tokens, segments, label_masks]) 218 | y_true = extract_label(labels, label_masks) 219 | 220 | y_pred = np.concatenate(y_pred) 221 | y_true = np.concatenate(y_true) 222 | R += y_pred.sum() 223 | P += y_true.sum() 224 | TP += ((y_pred + y_true) > 1).sum() 225 | 226 | # print(P, R, TP) 227 | pre = TP / R 228 | rec = TP / P 229 | 230 | return 2 * (pre * rec) / (pre + rec) 231 | 232 | 233 | def write_to_file(pred, file_path='test_submission.tsv'): 234 | """ 235 | 将结果写入文件,方便提交 236 | """ 237 | ret = [] 238 | for (d, p) in zip(test_data, pred): 239 | one_ret = [] 240 | q_id = d['query_id'] 241 | reply = d['reply'] 242 | for i, r in enumerate(reply): 243 | one_ret.append([q_id, r[0], p[i]]) 244 | 245 | ret.extend(one_ret) 246 | 247 | # write to file 248 | with open(file_path, 'w') as f: 249 | for r in ret: 250 | f.write('\t'.join([str(i) for i in r]) + '\n') 251 | 252 | 253 | class Evaluator(keras.callbacks.Callback): 254 | def __init__(self): 255 | self.best_f1 = 0. 256 | 257 | def on_epoch_end(self, eopch, logs=None): 258 | f1 = evaluate(valid_generator) 259 | if f1 > self.best_f1: 260 | self.best_f1 = f1 261 | self.model.save_weights('best_model.weights') 262 | 263 | print('f1: {}, best f1: {}'.format(f1, self.best_f1)) 264 | 265 | 266 | if __name__ == '__main__': 267 | evaluator = Evaluator() 268 | train_model.fit_generator( 269 | train_generator.generator(), 270 | steps_per_epoch=len(train_generator), 271 | epochs=5, 272 | callbacks=[evaluator] 273 | ) 274 | # load best weights 275 | train_model.load_weights('best_model.weights') 276 | # pred 277 | test_pred = [] 278 | for (t, s, l, m), _ in tqdm(test_generator): 279 | p = predict([t, s, m]) 280 | test_pred.extend(p) 281 | 282 | # write to file 283 | write_to_file(test_pred) 284 | else: 285 | infer_model.load_weights('best_model.weights') 286 | -------------------------------------------------------------------------------- /data/new_dict.txt: -------------------------------------------------------------------------------- 1 | 毛坯吗 2 | 带院的 3 | 您先看下 4 | 是多少 5 | 随时联系 6 | 毛坯的 7 | 不靠近 8 | 带装修 9 | 6号线 10 | 南北通透 11 | 有钥匙 12 | 您看看 13 | 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 | 增值税5 56 | 契税1 57 | 个税1 58 | 这个项目 59 | 学区划分 60 | 公立的 61 | 看房方便 62 | 我帮您约 63 | 不挡光 64 | 还可以 65 | 2楼的 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 | 21楼 122 | 挺好的 123 | 联系你 124 | 家具家电 125 | 两个月 126 | 带家具吗 127 | 价空间 128 | 在外地 129 | 不限购 130 | 也不会 131 | 框架的 132 | 会考虑吗 133 | 我第一 134 | 是临街的 135 | 这个小区 136 | 不带电梯 137 | 不错的 138 | 小区里面 139 | 有租户 140 | 诚意卖 141 | 我想了解 142 | 的咨询 143 | 生活愉快 144 | 不满两年 145 | 不是很高 146 | 如果对 147 | 一个小 148 | 满五唯一 149 | 配套都 150 | 税费高 151 | 不唯一 152 | 优质的 153 | 能看吗 154 | 可以看 155 | 如果觉得 156 | 上哪个 157 | 正常首付 158 | 一层的 159 | 750万 160 | 是想要 161 | 配套设施 162 | 您稍等 163 | 我帮您 164 | 算一下 165 | 附近有 166 | 卖不了 167 | 能看房吗 168 | 预约一下 169 | 85万 170 | 25年 171 | 我找一下 172 | 装修过 173 | 你预算 174 | 在哪里 175 | 契税和 176 | 是自住 177 | 我邀请 178 | 方便看吗 179 | 要提前 180 | 单元的 181 | 先看房 182 | 有客户 183 | 这种房子 184 | 不客气 185 | 微信号 186 | 国际小学 187 | 幼儿园到 188 | 微信吗 189 | 方便的话 190 | 我加下 191 | 你微信 192 | 首付5成 193 | 提前约 194 | 这边想要 195 | 的需求 196 | 是多少呢 197 | 要全款吗 198 | 这样子 199 | 这几天 200 | 您觉得 201 | 符合您 202 | 对楼层 203 | 应该是 204 | 业主说 205 | 要根据 206 | 360万 207 | 36万 208 | 一套成交 209 | 89万 210 | 95万 211 | 南向的 212 | 稍等哈 213 | 心里价位 214 | 那肯定 215 | 有产证的 216 | 回迁的 217 | 税多少 218 | 70年 219 | 最低多钱 220 | 房主聊聊 221 | 链家网 222 | 感兴趣吗 223 | 这样啊 224 | 不知道 225 | 不愿意 226 | 靠马路 227 | 消息了 228 | 99平米 229 | 在出售 230 | 能谈吗 231 | 30万 232 | 很久了 233 | 可以看吗 234 | 确定好 235 | 跟您说 236 | 可以看看 237 | 14点 238 | 门口见 239 | 旁边有 240 | 实验小学 241 | 私立的 242 | 在售的 243 | 一样的 244 | 都可以 245 | 是精装 246 | 有照片 247 | 都是精装 248 | 如果有空 249 | 是贝壳 250 | 是几期的 251 | 是三期的 252 | 是老证 253 | 还是蛮 254 | 他这个 255 | 需要自己 256 | 肯定是 257 | 满意了 258 | 是很好的 259 | 能看到 260 | 我及时 261 | 通知您 262 | 卖不卖 263 | 考虑吗 264 | 18年 265 | 去年年底 266 | 没问题 267 | 我发给你 268 | 600万 269 | 不需要 270 | 下午能看 271 | 最近没 272 | 在郑州 273 | 才能看 274 | 230万 275 | 几次了 276 | 现场看看 277 | 离地铁口 278 | 号码多少 279 | 位置和 280 | 我约下 281 | 留一个 282 | 可以卖 283 | 没有证 284 | 看过吗 285 | 有点难 286 | 挂出来 287 | 有增值税 288 | 我想咨询 289 | 世纪城 290 | 新上了 291 | 我约好了 292 | 来看房 293 | 家私家电 294 | 您想要 295 | 老证的 296 | 总楼层 297 | 28层 298 | 我明天 299 | 这几套 300 | 高一些 301 | 同户型 302 | 8楼的 303 | 365万 304 | 4万多 305 | 提前联系 306 | 首付三层 307 | 位置在哪 308 | 城那边 309 | 50万 310 | 不到了 311 | 首付3成 312 | 15楼 313 | 400万 314 | 是真实 315 | 最低多少 316 | 还没有 317 | 这种户型 318 | 不靠马路 319 | 靠路边 320 | 各方面 321 | 东向的 322 | 一直没 323 | 10分钟 324 | 多钱卖 325 | 满2年 326 | 需要缴纳 327 | 如果您 328 | 可以先看 329 | 能谈多少 330 | 多大空间 331 | 留个电话 332 | 受影响 333 | 咨询的 334 | 起来了 335 | 次新小区 336 | 对应的 337 | 骚扰你 338 | 花园二期 339 | 是商业 340 | 贵一点 341 | 住宅性质 342 | 二层的 343 | 不清楚 344 | 靠近公 345 | 约几点 346 | 我约一下 347 | 112万 348 | 证件齐全 349 | 挂牌出售 350 | 加个微信 351 | 方便联系 352 | 满几年 353 | 不用交 354 | 我加您 355 | 我朋友圈 356 | 保养的 357 | 带您看下 358 | 可以谈 359 | 90万 360 | 可以直接 361 | 也比较 362 | 是想买 363 | 个微信 364 | 70万 365 | 67万 366 | 简单装修 367 | 您对楼层 368 | 要求吗 369 | 小学和 370 | 多找几套 371 | 一起看看 372 | 企业产 373 | 是什么 374 | 都方便 375 | 420万 376 | 路小学 377 | 对口的 378 | 是哪里 379 | 着急卖 380 | 方便看 381 | 不靠路 382 | 105万 383 | 看过了 384 | 下来了 385 | 个单元 386 | 种户型 387 | 方便加您 388 | 税费多少 389 | 网签价 390 | 我马上 391 | 450万 392 | 还可以谈 393 | 看一下 394 | 我发一下 395 | 80多 396 | 想看一下 397 | 可以随时 398 | 几个人 399 | 有差额 400 | 能帮助您 401 | 80万 402 | 主要看 403 | 面积大 404 | 不太多 405 | 郡的房子 406 | 也没有 407 | 贝壳所有 408 | 中间楼层 409 | 高楼层 410 | 也可以 411 | 的购房 412 | 整理一下 413 | 不满二 414 | 可以协商 415 | 多少预算 416 | 确定下 417 | 看看嘛 418 | 调价格 419 | 距离地铁 420 | 就可以 421 | 底价多少 422 | 在开车 423 | 的名片 424 | 19年 425 | 都能看 426 | 可以落户 427 | 10年 428 | 有证吗 429 | 主要考虑 430 | 首付和 431 | 首付三成 432 | 通透户型 433 | 保持的 434 | 特别好 435 | 问一下 436 | 四居室 437 | 明白了 438 | 有合适的 439 | 先生还是 440 | 哪个小学 441 | 要考虑 442 | 双学区 443 | 能不能谈 444 | 子母车位 445 | 能不能看 446 | 可以先 447 | 你想看 448 | 周边有 449 | 师附小 450 | 挺合适 451 | 聊一下 452 | 是想买个 453 | 想看看 454 | 来看下 455 | 需要预约 456 | 需要全款 457 | 什么学校 458 | 您对学校 459 | 其他的 460 | 最低什么 461 | 微信聊 462 | 155万 463 | 哪一套 464 | 还没回来 465 | 回来了 466 | 证在手 467 | 有抵押 468 | 你贵姓 469 | 95平 470 | 个电话 471 | 2层的 472 | 我觉得 473 | 告诉你 474 | 我知道 475 | 我问完 476 | 联系您 477 | 城的房子 478 | 退出来 479 | 很不错 480 | 2号线 481 | 可以看下 482 | 我带你 483 | 没有降价 484 | 刚刚在 485 | 是新出 486 | 实验中学 487 | 要看看 488 | 有毛坯吗 489 | 你想要 490 | 12楼 491 | 满两年了 492 | 大约几点 493 | 方便留下 494 | 高税吗 495 | 想看看房 496 | 比较多 497 | 您把您 498 | 58万 499 | 接受什么 500 | 你看过 501 | 通气的 502 | 一室一厅 503 | 七楼的 504 | 用公积金 505 | 集体户口 506 | 3成首付 507 | 给您说 508 | 两年了 509 | 没住人 510 | 可谈吗 511 | 比较方便 512 | 不是重点 513 | 是哪个 514 | 看一下嘛 515 | 小区中庭 516 | 不是顶楼 517 | 不能贷款 518 | 100万 519 | 300万 520 | 17万 521 | 意思是 522 | 路小学和 523 | 满二年 524 | 无贷款 525 | 今天刚 526 | 295万 527 | 一定要 528 | 5号线 529 | 这边想看 530 | 预算多少 531 | 468万 532 | 小区绿化 533 | 有空间 534 | 是想了解 535 | 4楼的 536 | 十三中 537 | 十六中 538 | 介绍一下 539 | 都可以看 540 | 还不错 541 | 过两年 542 | 出租的 543 | 卖的话 544 | 有一套 545 | 还没回 546 | 不带车位 547 | 首套房 548 | 证过二 549 | 你考虑 550 | 一期二期 551 | 基本都 552 | 9号楼 553 | 小区环境 554 | 15号楼 555 | 诚心卖 556 | 新出来 557 | 40万 558 | 房东急卖 559 | 万达广场 560 | 接受贷款 561 | 158万 562 | 也可以租 563 | 其次就是 564 | 175万 565 | 加装电梯 566 | 最便宜的 567 | 27楼 568 | 基本没 569 | 生活配套 570 | 很合适 571 | 来看一下 572 | 周边配套 573 | 东区的 574 | 也不远 575 | 可以直 576 | 户型很好 577 | 税费很高 578 | 想了解 579 | 能贷多少 580 | 是考虑 581 | 您考虑几 582 | 30年 583 | 我约好 584 | 在卖的 585 | 有哪些 586 | 总价低 587 | 税费情况 588 | 两个点 589 | 你发给我 590 | 几年了 591 | 产权过二 592 | 10万 593 | 20万 594 | 大空间 595 | 我同事 596 | 165万 597 | 能商量 598 | 最好的 599 | 老人住 600 | 想换房 601 | 影响不大 602 | 业主没说 603 | 我微信 604 | 6楼的 605 | 花园小区 606 | 可以做 607 | 总共有 608 | 这两天 609 | 价格也 610 | 带客户 611 | 还考虑 612 | 是想考虑 613 | 装修咋样 614 | 带地暖 615 | 还有其他 616 | 啥价位 617 | 贷款吗 618 | 可以贷款 619 | 是毛胚 620 | 您想看 621 | 有空间吗 622 | 见面谈 623 | 中高层楼 624 | 麻烦您 625 | 您之前 626 | 给您看看 627 | 大三房 628 | 中高楼层 629 | 采光充足 630 | 410万 631 | 我问问 632 | 房子还在 633 | 60万 634 | 特别大 635 | 一居室 636 | 四层的 637 | 个客户 638 | 35万 639 | 跟你说 640 | 390万 641 | 相中了 642 | 可以商量 643 | 谈价格 644 | 还有一个 645 | 四楼的 646 | 还没说 647 | 出来了 648 | 挡光吗 649 | 很熟悉 650 | 能看嘛 651 | 您放心 652 | 两梯两户 653 | 便宜一些 654 | 小高层 655 | 采光如何 656 | 湖小学 657 | 山小学 658 | 湖中学 659 | 下午几点 660 | 有兴趣 661 | 黑科技 662 | 比较抢手 663 | 业主自住 664 | 一起看 665 | 550万 666 | 二手买进 667 | 满几年了 668 | 没有产证 669 | 不是很 670 | 是想看 671 | 我朋友 672 | 装修完 673 | 自己住 674 | 几个月 675 | 别的房子 676 | 毛坯交付 677 | 不太适合 678 | 还有一套 679 | 大概1 680 | 到多少 681 | 是首套房 682 | 46万 683 | 二套房 684 | 首付六成 685 | 92万 686 | 什么学位 687 | 车位呢 688 | 周围配套 689 | 能接受 690 | 是几期 691 | 刚发的 692 | 单价低 693 | 可以接受 694 | 园小区 695 | 有优惠 696 | 无抵押 697 | 房产证吗 698 | 不受影响 699 | 首付多少 700 | 什么时间 701 | 不是毛坯 702 | 老小区 703 | 比较好 704 | 带您看 705 | 大平层 706 | 多少啊 707 | 想问一下 708 | 我微信上 709 | 给你发 710 | 很优质 711 | 明天有空 712 | 最低能 713 | 房主自住 714 | 您想咨询 715 | 主要是 716 | 这边买房 717 | 交易不了 718 | 其他小区 719 | 山中学 720 | 发给我 721 | 在哪个 722 | 回复你 723 | 我联系 724 | 大概几点 725 | 贷款记录 726 | 加一起 727 | 24万 728 | 可以去看 729 | 下午2 730 | 有遮挡吗 731 | 人车分流 732 | 4号线 733 | 我们会 734 | 马路吗 735 | 430万 736 | 契税首套 737 | 了解一下 738 | 业主没卖 739 | 房子满意 740 | 有一个 741 | 差额税 742 | 6万左右 743 | VR看房 744 | 谈价空间 745 | 可以帮到 746 | 还是只 747 | 几点呢 748 | 比较近 749 | 800米 750 | 您对学区 751 | 读什么 752 | 要交多少 753 | 能卖吗 754 | 这套房源 755 | 五楼的 756 | 确定了 757 | 不临路 758 | 能不能 759 | 约一下 760 | 给您找 761 | 安排一下 762 | 您贵姓 763 | 提前和 764 | 都能看到 765 | 点进来 766 | 诚心买 767 | 说一下 768 | 优质房源 769 | 首付几成 770 | 带您看房 771 | 贷款还清 772 | 不满2 773 | 10月份 774 | 在链家 775 | 12月 776 | 实地看 777 | 要看看吗 778 | 只有一个 779 | 你想什么 780 | 980万 781 | 有遮挡 782 | 可以买 783 | 是怎样的 784 | 中介费3 785 | 环境好 786 | 就不行了 787 | 位置好 788 | 你知道 789 | 几室的 790 | 我们贝壳 791 | 两室的 792 | 业主底价 793 | 55万 794 | 几号院 795 | 2号院 796 | 太大了 797 | 咱们先看 798 | 20年 799 | 介绍下 800 | 价格可谈 801 | 视野好 802 | 18楼的 803 | 系统上 804 | 有产证吗 805 | 师范附小 806 | 中本部 807 | 有产证 808 | 正常交易 809 | 也只有 810 | 一个人 811 | 点击同意 812 | 对口小学 813 | 你说的 814 | 下房主 815 | 想办法 816 | 需要换房 817 | 业主也没 818 | 9楼的 819 | 小区里 820 | 是首套 821 | 我问下 822 | 过来看看 823 | 我刚刚问 824 | 在附近 825 | 没怎么 826 | 89平 827 | 空间不大 828 | 不满五 829 | 北边是 830 | 在哪啊 831 | 一直在 832 | 刚登记 833 | 给你看看 834 | 有太阳 835 | 采光不错 836 | 重新装 837 | 不靠路边 838 | 我带您 839 | 过去看看 840 | 离马路 841 | 首付全 842 | 560万 843 | 孩子在 844 | 只考虑 845 | 住在附近 846 | 给我留 847 | 给您发 848 | 考虑几 849 | 推荐几套 850 | 得五一 851 | 我给您 852 | 也不是 853 | 我想看 854 | 村小学 855 | 店经理 856 | 在售房源 857 | 套房源 858 | 交易过 859 | 预算大概 860 | 是多少呀 861 | 两室一厅 862 | 点击进入 863 | 微信同步 864 | 中介费2 865 | 65万 866 | 有大税吗 867 | 刚降价 868 | 这样吧 869 | 你看看 870 | 几套房 871 | 你觉得 872 | 挺便宜的 873 | 最低价位 874 | 几栋楼 875 | 不把边 876 | 五十中 877 | 税大概 878 | 只有契税 879 | 二套3 880 | 什么样的 881 | 74万 882 | 什么情况 883 | 可以把您 884 | 电话给我 885 | 业主也 886 | 235万 887 | 都没有 888 | 比较大 889 | 这栋楼 890 | 含车位 891 | 您先看房 892 | 房东沟通 893 | 房东谈价 894 | 想去看看 895 | 诚意出售 896 | 给我们 897 | 正常出售 898 | 都比较 899 | 5年级 900 | 11月份 901 | 8栋的 902 | 比较高 903 | 能看看吗 904 | 回来后 905 | 您看过 906 | 哪里呢 907 | 约几套 908 | 不行就 909 | 打扰您 910 | 带你看房 911 | 年买的 912 | 带窗户 913 | 不满二年 914 | 二梯队 915 | 二期的 916 | 我加一下 917 | 房源信息 918 | 需要交 919 | 留一下 920 | 一起卖吗 921 | 也要看 922 | 和业主谈 923 | 看下吧 924 | 两梯四户 925 | 采光和 926 | 我和业主 927 | 比较熟 928 | 实地看房 929 | 装修好了 930 | 8号楼 931 | 多久过来 932 | 15分钟 933 | 5分钟 934 | 是想找 935 | 我带客户 936 | 实地看过 937 | 有空吗 938 | 您微信 939 | 19楼 940 | 微信上 941 | 带车位吗 942 | 带车位 943 | 没住过人 944 | 我发一个 945 | 税费2 946 | 没有占用 947 | 有证啊 948 | 价格高点 949 | 比较快 950 | 视野开阔 951 | 休息了 952 | 加微信 953 | 满5年 954 | 首套1 955 | 需要提前 956 | 我给你 957 | 讲解一下 958 | 350万 959 | 有点远 960 | 680万 961 | 低一点 962 | 见面沟通 963 | 打错了 964 | 总高6层 965 | 不超过 966 | 没有办法 967 | vr带看 968 | 基本没有 969 | 确定要 970 | 房东说 971 | 实地看看 972 | 很高兴 973 | 带露台的 974 | 要不要看 975 | 要置换吗 976 | 今天晚上 977 | 我们公司 978 | 电话联系 979 | 更方便 980 | 方便看房 981 | 通过VR 982 | 业主换房 983 | 我建议 984 | 带家私 985 | 价格能谈 986 | 6号楼 987 | 6层的 988 | 多大面积 989 | 看房子吗 990 | 110万 991 | 73万 992 | 微信沟通 993 | 带您看看 994 | 靠路吗 995 | 性价比高 996 | 二楼的 997 | 产证在手 998 | 满三满二 999 | 买的话 1000 | 房龄新 1001 | 环境也 1002 | 是链家 1003 | 给你看 1004 | 带院子 1005 | 具体看 1006 | 我带你看 1007 | 跟业主谈 1008 | 有优势 1009 | 不太方便 1010 | 因为疫情 1011 | 想看房 1012 | 75万 1013 | 是哪所 1014 | 实验二小 1015 | 街小学 1016 | 贵一些 1017 | 能接受吗 1018 | 比较优质 1019 | 不合适 1020 | 明天上午 1021 | 215万 1022 | 你同意 1023 | 可以改 1024 | 哪天方便 1025 | 中学区 1026 | 是公寓 1027 | 在网上 1028 | 详细的 1029 | 是买来 1030 | 价格还有 1031 | 如果满意 1032 | 可以约 1033 | 离高铁 1034 | 一栋楼 1035 | 830万 1036 | 您想了解 1037 | 发一下 1038 | 能落户 1039 | 还能谈 1040 | VR带看 1041 | 在线上 1042 | 首付4成 1043 | 是顶层 1044 | 您贵姓啊 1045 | 和业主聊 1046 | 这两套 1047 | 要不要 1048 | 稳定了 1049 | 啥时候看 1050 | 个微信吧 1051 | 沟通一下 1052 | 一下吧 1053 | 没有遮挡 1054 | 可以看到 1055 | 特别方便 1056 | 也比较多 1057 | 总高30 1058 | 关注一下 1059 | 我维护 1060 | 带看过 1061 | 比较熟悉 1062 | 的情况 1063 | 有大税 1064 | 满二年了 1065 | 刚才那套 1066 | 大学附近 1067 | 购房资格 1068 | 也很方便 1069 | 是换房吗 1070 | 过来看房 1071 | 挺合适的 1072 | 苑小区 1073 | 两个人 1074 | 比较适合 1075 | 大产权 1076 | 对口什么 1077 | 再看看 1078 | 还有什么 1079 | 还有空间 1080 | 装修了吗 1081 | 接受不了 1082 | 能贷款吗 1083 | 哪几套 1084 | 就能卖 1085 | 也不好 1086 | vr看房 1087 | 点进去 1088 | 出租了 1089 | 不着急 1090 | 租出去了 1091 | 关注的 1092 | 产权车位 1093 | 方便留个 1094 | 桥小学 1095 | 要带走 1096 | 不靠高架 1097 | 有优惠吗 1098 | 浮动不大 1099 | 咨询过 1100 | 只需要 1101 | 你电话 1102 | 以上了 1103 | 不是靠 1104 | 未满两年 1105 | 学校都在 1106 | 不是首套 1107 | 这两个 1108 | 哪里的 1109 | 要提前约 1110 | 看一下哈 1111 | 路校区 1112 | 西路小学 1113 | 给你说 1114 | 电梯洋房 1115 | 您参考 1116 | 1号楼 1117 | 对外面 1118 | 采光很好 1119 | 最南边 1120 | 带电梯 1121 | 带家具 1122 | 离小区 1123 | 的名字 1124 | 合适了 1125 | 是德佑 1126 | 微信同号 1127 | 没有声音 1128 | 可以放心 1129 | 都很高 1130 | 南北通 1131 | 装修好 1132 | 90平 1133 | 带电梯的 1134 | 在出租 1135 | 签不了 1136 | 不确定 1137 | 初中高中 1138 | 啥时候满 1139 | 120万 1140 | 128万 1141 | 给您回话 1142 | 跟我们 1143 | 对口中学 1144 | 理工附中 1145 | 大概多少 1146 | 路中学 1147 | 没遮挡 1148 | 维护人 1149 | 中间位置 1150 | 520万 1151 | 怎么付款 1152 | 跟房东 1153 | 心里价格 1154 | 要求么 1155 | 2单元 1156 | 湘江道 1157 | 价格空间 1158 | 这个价 1159 | 先看房子 1160 | 租客在住 1161 | 带阳台 1162 | 里小学 1163 | 把房子 1164 | 定下来 1165 | 870万 1166 | 900万 1167 | 还是想 1168 | 10号楼 1169 | 我找找 1170 | 系统里 1171 | 63万 1172 | 区的房子 1173 | 只能看 1174 | 我说的 1175 | 啥时间 1176 | 我刚刚 1177 | 非常方便 1178 | 告诉我 1179 | 不见面 1180 | 说低价 1181 | 过来开门 1182 | 都是毛坯 1183 | 我发你 1184 | 我们链家 1185 | 是重庆 1186 | 东边户 1187 | 怎么称呼 1188 | 有希望 1189 | 具体多少 1190 | 新楼盘 1191 | 新城中学 1192 | 对周边 1193 | 方便加下 1194 | 没办法 1195 | 挂牌价格 1196 | 135万 1197 | 会高一些 1198 | 3楼的 1199 | 大概在 1200 | 业主配合 1201 | 大概什么 1202 | 没有照片 1203 | 有产权 1204 | 先看一下 1205 | 浏览一下 1206 | 加我微信 1207 | 比较少 1208 | 16万 1209 | 正在建设 1210 | 目前报价 1211 | 我回头 1212 | 位置很好 1213 | 还有点 1214 | 大花园 1215 | 民水民电 1216 | 可以按揭 1217 | 只能全款 1218 | 远一点 1219 | 有两套 1220 | 是多层 1221 | 有双税 1222 | 想卖了 1223 | 二套贷款 1224 | 42万 1225 | 装修保养 1226 | 240万 1227 | 了解下 1228 | 我提前 1229 | 啥样的 1230 | 那套房子 1231 | 225万 1232 | 实验学区 1233 | 近地铁 1234 | 要约业主 1235 | 跟我说下 1236 | 5号楼 1237 | 多久满二 1238 | 觉得房子 1239 | 居家装修 1240 | 您对装修 1241 | 还有一 1242 | 也很好 1243 | 首付要 1244 | 无增值税 1245 | 你放心 1246 | 有人住 1247 | 很多次 1248 | 跟业主 1249 | 也比较好 1250 | 每个客户 1251 | 要约租给 1252 | 我建议您 1253 | 来之后 1254 | 很多细节 1255 | 小学情况 1256 | 都是新 1257 | 会带走 1258 | 给业主 1259 | 发错了 1260 | 50年 1261 | 接受多少 1262 | 首套购房 1263 | 都差不多 1264 | 您微信吧 1265 | 购房资质 1266 | 知道了 1267 | 这几个 1268 | 肯定不会 1269 | 打给您 1270 | 明天早上 1271 | 我也没 1272 | 楼龄新 1273 | 7楼的 1274 | 户型跟 1275 | 我发给您 1276 | vR看房 1277 | 可以读 1278 | 为了孩子 1279 | 房东自住 1280 | 组客户 1281 | 家电齐全 1282 | 西北向 1283 | 帮您约 1284 | 可以加我 1285 | 中午几点 1286 | 我刚问 1287 | 三个电话 1288 | 打了两个 1289 | 以后再约 1290 | 3号楼 1291 | 您看下 1292 | 4层的 1293 | 没人谈过 1294 | 我维护的 1295 | 适合您 1296 | 我给您推 1297 | 88平 1298 | 2号楼 1299 | 您说下 1300 | 给您匹配 1301 | 刚才在 1302 | 离地铁站 1303 | 也很近 1304 | 有关系 1305 | 33万 1306 | 我想看房 1307 | 385万 1308 | 来约一下 1309 | 满三年了 1310 | 三个点 1311 | 也比较大 1312 | 户型报告 1313 | 238万 1314 | 着急用钱 1315 | 720万 1316 | 700万 1317 | 卧朝南 1318 | 14楼 1319 | 16楼 1320 | 西中学 1321 | 一个微信 1322 | 刚登记的 1323 | 11楼 1324 | 很难了 1325 | 对吧对 1326 | 多少空间 1327 | 您看过吗 1328 | 觉得满意 1329 | 比较喜欢 1330 | 140万 1331 | 一手房 1332 | 去看过吗 1333 | 134万 1334 | 87万 1335 | 有点高 1336 | 光华大道 1337 | 刚交房 1338 | 办下来了 1339 | 一起看下 1340 | 17年 1341 | 付多少 1342 | 挺不错的 1343 | 加起来 1344 | 我负责 1345 | 一室的 1346 | 68万 1347 | 要求全款 1348 | 我说下 1349 | 有客户出 1350 | 首付30 1351 | 客气了 1352 | 贷款多少 1353 | 需要卖 1354 | 每个房 1355 | 都朝南 1356 | 是南北 1357 | 我问问哈 1358 | 中学直升 1359 | 楼层低 1360 | 楼层低点 1361 | 17楼 1362 | 商业配套 1363 | 78万 1364 | 户型方正 1365 | 税费怎么 1366 | 26万 1367 | 275万 1368 | 多少钱啊 1369 | 楼层和 1370 | 有差额税 1371 | 跟我说 1372 | 明天几点 1373 | 六楼的 1374 | 卖的客户 1375 | 5月份 1376 | 觉得合适 1377 | 装修比 1378 | 处理一下 1379 | 能降多少 1380 | 您微信吗 1381 | 在外网 1382 | 过来看 1383 | 给您回 1384 | 三居室吗 1385 | 可以贷 1386 | 84万 1387 | 余额的 1388 | 多余的 1389 | 几个客户 1390 | 下班了 1391 | 一般都是 1392 | 房主承担 1393 | 800万 1394 | 610万 1395 | 再谈谈 1396 | 需要置换 1397 | 可以把 1398 | 没见过 1399 | 间距大 1400 | 东北向 1401 | 业主只 1402 | 看过之后 1403 | 160万 1404 | 也是很 1405 | 个顶楼 1406 | 不满五年 1407 | 经济适用 1408 | 也就是 1409 | 不能看 1410 | 再谈价格 1411 | 找到了 1412 | 半个月 1413 | 不能落户 1414 | 号码给我 1415 | 你要是 1416 | 103平 1417 | 成交信息 1418 | 不满2年 1419 | 的情况下 1420 | 优点就是 1421 | 我去看过 1422 | 12号楼 1423 | 早点休息 1424 | 交多少 1425 | 还能讲价 1426 | 不好说 1427 | 150万 1428 | 宝安中学 1429 | 可以参考 1430 | 我想明天 1431 | 推荐一下 1432 | 没有大税 1433 | 红本在手 1434 | 贝壳平台 1435 | 山庄的 1436 | 带露台 1437 | 带你看看 1438 | 320万 1439 | 园的房子 1440 | 号楼的 1441 | 交契税 1442 | 去看看 1443 | 真房源 1444 | 竭诚为您 1445 | 最优质的 1446 | 小学初中 1447 | 多久了 1448 | 三个月 1449 | 对学区 1450 | 给你找 1451 | 270万 1452 | 都不卖 1453 | 一贯制的 1454 | 可以谈点 1455 | 田小学 1456 | 啥情况 1457 | 可以帮助 1458 | 线上带看 1459 | 大概多久 1460 | 孩子还没 1461 | 推荐给你 1462 | 等满二 1463 | 小卧室 1464 | 其他都 1465 | 有意向 1466 | 不让看 1467 | 小三居 1468 | 家具都带 1469 | 190万 1470 | 98万 1471 | 动迁的 1472 | 大附中 1473 | 说950 1474 | 划算一点 1475 | 多少呢 1476 | 没满二 1477 | 8万左右 1478 | 今天能看 1479 | 低价多少 1480 | 我再帮你 1481 | 愿意卖 1482 | 可以看嘛 1483 | 手机上 1484 | 能贷款 1485 | 可谈空间 1486 | 5年了 1487 | 咱这边 1488 | 也不错 1489 | 5楼的 1490 | 340万 1491 | 今天有空 1492 | 就卖了 1493 | 次顶楼 1494 | 顶楼复式 1495 | 最便宜 1496 | 您留个 1497 | 贝壳找房 1498 | 4万左右 1499 | 楼王位置 1500 | 多久有空 1501 | 15万 1502 | 您对楼 1503 | 客户出 1504 | 才卖的 1505 | 为您匹配 1506 | 首付五成 1507 | 配合满二 1508 | 房主包税 1509 | 装修花了 1510 | 20多 1511 | 就可以读 1512 | 130万 1513 | 在3楼 1514 | 来看过 1515 | 不占用 1516 | 加您微信 1517 | 13万 1518 | 在VR 1519 | 给您讲解 1520 | 因为他 1521 | 两套房子 1522 | 另一套 1523 | 其他地方 1524 | 有租客 1525 | 方便留 1526 | 425万 1527 | 有影响吗 1528 | 会考虑 1529 | 500万 1530 | 城公寓 1531 | 特殊情况 1532 | 到明年 1533 | 离地铁 1534 | 先看看房 1535 | 不用了 1536 | 直接买 1537 | 和家人 1538 | 不是很大 1539 | 带装修的 1540 | 您想找 1541 | 装修怎样 1542 | 还没拍 1543 | 推荐给您 1544 | 能谈到 1545 | 有一定 1546 | 业主刚 1547 | 是顶楼 1548 | 都没卖 1549 | 大三室 1550 | 带看吗 1551 | 得房率 1552 | 21世纪 1553 | 不动产证 1554 | 比较老 1555 | 15年 1556 | 房东谈谈 1557 | 咱们约 1558 | 橡树湾 1559 | 肯定要 1560 | 都是租的 1561 | 没有车位 1562 | 有车位 1563 | 2室一厅 1564 | 有赠送 1565 | 100平 1566 | 什么学区 1567 | 缺点就是 1568 | 着急出售 1569 | 我问一下 1570 | 97万 1571 | 低楼层 1572 | 装修很 1573 | 湾的房子 1574 | 自己住的 1575 | 我给你约 1576 | 您发的 1577 | 加税费 1578 | 贷款也就 1579 | 没有任何 1580 | 对面就是 1581 | 明天能看 1582 | 已经卖了 1583 | 也挺好的 1584 | 是省一级 1585 | 小学直升 1586 | 可以实地 1587 | 290万 1588 | 便宜一点 1589 | 业主心态 1590 | 都比较高 1591 | 府的房子 1592 | 价格最低 1593 | 全款购买 1594 | 税满五年 1595 | 还是西边 1596 | 西边户 1597 | 低层楼 1598 | 小区门口 1599 | 500米 1600 | 道小学 1601 | 真实在售 1602 | 小学读 1603 | 南北户型 1604 | 差不太多 1605 | 不稳定 1606 | 查一下 1607 | 不包税 1608 | 我发起 1609 | 可以帮你 1610 | 买公积金 1611 | 拿出来 1612 | 122万 1613 | 商业性质 1614 | 出门就是 1615 | 产权清晰 1616 | 小三室 1617 | 安排好 1618 | 哪里啊 1619 | 这种情况 1620 | 坐下来谈 1621 | 点左右 1622 | 过来了吗 1623 | 6栋的 1624 | 你发的 1625 | 河小学 1626 | 几年级了 1627 | 可以用 1628 | 370万 1629 | 上学呢 1630 | 首付大概 1631 | 7万左右 1632 | 好像没有 1633 | 网上看 1634 | 银行评估 1635 | 都有什么 1636 | 我加你 1637 | 没满五 1638 | 高一点 1639 | 优惠多少 1640 | 有降价 1641 | 是最低了 1642 | 之前报价 1643 | 您贵姓呢 1644 | b区的 1645 | 房本吗 1646 | 在北京 1647 | 您发给我 1648 | 北区的 1649 | 空间不多 1650 | 证满两年 1651 | 个税契税 1652 | 大红本 1653 | 车位另算 1654 | 2月份 1655 | 那我明天 1656 | 不把山 1657 | 是否需要 1658 | 目前只有 1659 | 上海道 1660 | 看一眼 1661 | 就不能 1662 | 价格可议 1663 | 电话沟通 1664 | 还是挺多 1665 | 比较贵 1666 | 给你推荐 1667 | 打扰你 1668 | 53万 1669 | 临路吗 1670 | 能便宜吗 1671 | 联系我 1672 | 装修如何 1673 | 通透的 1674 | 贷款利息 1675 | 也差不多 1676 | 价格还能 1677 | 周围有 1678 | 没住过 1679 | 附近还有 1680 | 这样说的 1681 | 采光咋样 1682 | 独立的 1683 | 我先约 1684 | 就知道了 1685 | 在南京 1686 | 您想买 1687 | 肯定会 1688 | 的朋友圈 1689 | 适合投资 1690 | 全款还是 1691 | 配套完善 1692 | 安小学 1693 | 安中学 1694 | 考虑嘛 1695 | 方便沟通 1696 | 在建的 1697 | 一个单元 1698 | 首套首贷 1699 | 问下房东 1700 | 毛胚房 1701 | 需要考 1702 | 我去帮您 1703 | 蛮好的 1704 | 根据您 1705 | 二十中 1706 | 证过几 1707 | 林中学 1708 | 心理价位 1709 | 成交过 1710 | 没有太多 1711 | 24楼 1712 | 朝马路 1713 | 多久到 1714 | 契税票 1715 | 就只有 1716 | 首付得 1717 | 不能谈 1718 | 电话和 1719 | 具体要求 1720 | 有图片 1721 | 过去看 1722 | 房东谈 1723 | 首套二套 1724 | 计算一下 1725 | 125万 1726 | 170万 1727 | 您找到 1728 | 这样嘛 1729 | 再联系 1730 | 十八中 1731 | 具体谈了 1732 | 价格高 1733 | 降价空间 1734 | 311万 1735 | 286万 1736 | 278万 1737 | 不满意 1738 | 还是商贷 1739 | 还能再 1740 | 几个小区 1741 | 515万 1742 | 安排看房 1743 | 业主承担 1744 | 也不算 1745 | 约下房东 1746 | 下午能 1747 | 一梯两户 1748 | 不能说 1749 | 给您电话 1750 | 不过去 1751 | 带家电 1752 | 我去接 1753 | 有阳光吗 1754 | 找一个 1755 | 详细说明 1756 | 不包含 1757 | 9月份 1758 | 不能交易 1759 | 您想考虑 1760 | 每个房子 1761 | 或者微信 1762 | 200万 1763 | 青羊实验 1764 | 只有一套 1765 | 19号 1766 | 还在沟通 1767 | 房子都没 1768 | 在国外 1769 | 园都是 1770 | 疫情期间 1771 | 您考虑买 1772 | 一共有 1773 | 湿地公园 1774 | 去看过 1775 | 前几天 1776 | 115万 1777 | 都是买方 1778 | 看过了吗 1779 | 你想几点 1780 | 5万多 1781 | 组合贷 1782 | 没满两年 1783 | 喜欢的话 1784 | 没有抵押 1785 | 有点吵 1786 | 还是西户 1787 | 再等等 1788 | 10多 1789 | 的事情 1790 | 距离马路 1791 | 小区外面 1792 | 我刚才 1793 | 给您发送 1794 | 东西向的 1795 | 每个人 1796 | 对朝向 1797 | 也不用 1798 | 还不清楚 1799 | 我跟业主 1800 | 清楚了 1801 | 28万 1802 | 加一下 1803 | 广场和 1804 | 小区最后 1805 | 过几天 1806 | 大概15 1807 | 其它的 1808 | 我们也 1809 | 是哪些 1810 | 说底价 1811 | 哈嗯嗯 1812 | 屋里的 1813 | 133万 1814 | 310万 1815 | 路地铁站 1816 | 分钟左右 1817 | 约好了 1818 | 给你回复 1819 | 我们也有 1820 | 2居室 1821 | 估计能 1822 | 可以做低 1823 | 最低首付 1824 | 首付最低 1825 | 看恒大 1826 | 咨询我 1827 | 直接买进 1828 | 在小区最 1829 | 家电家具 1830 | 按揭贷款 1831 | 出价多少 1832 | 有其它 1833 | 2室的 1834 | 要买房 1835 | 靠近马路 1836 | 得提前约 1837 | 好像不 1838 | 岗小学 1839 | 离学校 1840 | 在外面 1841 | 对口学校 1842 | 同小区 1843 | 给孩子 1844 | 几万块钱 1845 | 还能商量 1846 | 在家里 1847 | 可以留个 1848 | 我打电话 1849 | 交的房 1850 | 无大税 1851 | 税费低 1852 | 是租给 1853 | 比较安静 1854 | 这几年 1855 | 很安静 1856 | 临街不 1857 | 几栋的 1858 | 两万吧 1859 | 房主谈 1860 | 在协商 1861 | 重新装修 1862 | 遮挡吗 1863 | 你要不 1864 | 下同意 1865 | 公馆小区 1866 | 不好说啊 1867 | 大三居 1868 | 更清楚 1869 | 8号线 1870 | 下午好 1871 | 200米 1872 | 情况下 1873 | 非常近 1874 | 通知你 1875 | 小区停车 1876 | 门小学 1877 | 问题不大 1878 | 还是毛坯 1879 | 卧室朝南 1880 | 客厅朝北 1881 | 使用面积 1882 | 06年 1883 | 12年 1884 | 57万 1885 | 相差不大 1886 | 个VR 1887 | 容积率为 1888 | 谈一些 1889 | 出租中 1890 | 530万 1891 | 谈价钱 1892 | 在跟他 1893 | 多大了 1894 | 贝壳上 1895 | 付首付 1896 | 下房本 1897 | 白天能看 1898 | 我们都 1899 | 您留下 1900 | 我给您回 1901 | 在建设 1902 | 房东面谈 1903 | 中低层楼 1904 | 你打算 1905 | 我得问问 1906 | 如果想 1907 | 厅户型 1908 | 刚挂牌 1909 | 读哪个 1910 | 苑的房子 1911 | 湖花园 1912 | 进入VR 1913 | 耽误您 1914 | 医院和 1915 | 2万左右 1916 | 11层 1917 | 就能看 1918 | 为单位 1919 | 90多 1920 | 确认一下 1921 | 民小学 1922 | 61万 1923 | 看完房子 1924 | 是整个 1925 | 16年 1926 | 肯定不行 1927 | 便宜多少 1928 | 10栋 1929 | 给您确定 1930 | 您打算 1931 | 正常贷款 1932 | 210万 1933 | 不用担心 1934 | 自住装修 1935 | 价格会比 1936 | 要过来 1937 | 哪一个 1938 | 是想租 1939 | 想买个 1940 | 免个税 1941 | 比较合适 1942 | 4月份 1943 | 330万 1944 | 考虑买 1945 | 3万左右 1946 | 45万 1947 | 坐下来 1948 | 1万左右 1949 | 线上看 1950 | 过五年 1951 | 房东商量 1952 | 等一下 1953 | 470万 1954 | 可以加下 1955 | 比较小 1956 | 咱们考虑 1957 | 下证了 1958 | 10号 1959 | 备注一下 1960 | 信息呢 1961 | 所中学 1962 | 不沿街 1963 | 南向两居 1964 | 龙光城 1965 | 有一部分 1966 | 必须要 1967 | 不准确 1968 | 满了吗 1969 | 非常优质 1970 | 证齐全 1971 | 您留一下 1972 | 考虑学区 1973 | 不含家具 1974 | 链接发 1975 | 微信说 1976 | 类似的 1977 | 房产政策 1978 | 买进价 1979 | 想卖掉 1980 | 也不多 1981 | 真实报价 1982 | 搬走了 1983 | 以及周边 1984 | 找一下 1985 | 要多少 1986 | 只需要交 1987 | 四万左右 1988 | 需要咨询 1989 | 拿到手 1990 | 具体时间 1991 | 大概想 1992 | 分享给您 1993 | 20多万 1994 | 一下同意 1995 | 260万 1996 | 能帮您 1997 | 没有人 1998 | 哈密道 1999 | 了解了解 2000 | 哪个小区 2001 | 明年年底 2002 | 着急住 2003 | 48中 2004 | 把东山 2005 | 三号线 2006 | 四年了 2007 | 不卖了 2008 | 桥中学 2009 | 给您回复 2010 | 是哪种 2011 | 学区名额 2012 | 205万 2013 | 你想了解 2014 | 了解过 2015 | 我建议你 2016 | 看下房 2017 | 国际城 2018 | 师附中 2019 | 具体需求 2020 | 采光怎样 2021 | 您想几点 2022 | 不会卖的 2023 | 要预约 2024 | 北朝向的 2025 | 便宜点 2026 | 问题没 2027 | 还是二套 2028 | 看房子吧 2029 | 公立小学 2030 | 低首付 2031 | 能直接 2032 | 贷多少 2033 | 也不带 2034 | 顺便看看 2035 | 可以租 2036 | 后排的 2037 | 沟通过 2038 | 学院的 2039 | 楼层高 2040 | 不会吵的 2041 | 在北面 2042 | 门口就是 2043 | 大概需求 2044 | 家孩子 2045 | 村的房子 2046 | 家庭名下 2047 | 卖掉了 2048 | 185万 2049 | 进小区 2050 | 不让进 2051 | 没有签约 2052 | 会比较 2053 | 我记得 2054 | 首付需要 2055 | 及周边 2056 | 首付比例 2057 | 你贵姓呢 2058 | 微信联系 2059 | 含税吗 2060 | 一条路 2061 | 德佑地产 2062 | 上午10 2063 | 100米 2064 | 地铁近 2065 | 配套成熟 2066 | 是清水 2067 | 贷款买的 2068 | 2梯4 2069 | 25楼的 2070 | 你看下 2071 | 村小区 2072 | 筛选一下 2073 | 业主急卖 2074 | 谈多少 2075 | 谈一下 2076 | 93万 2077 | 产证满二 2078 | 有租客在 2079 | 得多少钱 2080 | 很成熟 2081 | 路地铁口 2082 | 业主急售 2083 | 采光也 2084 | 次顶层 2085 | 我们店里 2086 | 贷款买房 2087 | 配套齐全 2088 | 年左右 2089 | 还不确定 2090 | 12万 2091 | 这个月 2092 | 没接电话 2093 | 提前约下 2094 | 只能买 2095 | 东路小学 2096 | 联系一下 2097 | 小区品质 2098 | 别墅区的 2099 | 桥小区 2100 | 师大附小 2101 | 都是真实 2102 | 南区北区 2103 | 可以迁 2104 | 十四中 2105 | 不是特别 2106 | 有证了 2107 | 这些都是 2108 | 留钥匙 2109 | 如果你 2110 | 最近还在 2111 | 几点到 2112 | 228万 2113 | 我加个 2114 | 挂学区 2115 | 可能不太 2116 | 30多万 2117 | 你先看看 2118 | 买二手房 2119 | 都可以卖 2120 | 天联系 2121 | 不着急卖 2122 | 没有特别 2123 | 给我回复 2124 | 面积段 2125 | 单价1 2126 | 能谈点 2127 | 也不满二 2128 | 就是西 2129 | 家园小区 2130 | 汇中学 2131 | 拿钥匙 2132 | 准备好了 2133 | 22楼 2134 | 今年刚 2135 | 微信聊吧 2136 | 具体楼层 2137 | 485万 2138 | 微信吧 2139 | 满了两年 2140 | 就不用 2141 | 不同意 2142 | 首套贷款 2143 | 要去看看 2144 | 动不了 2145 | 都在家 2146 | 不是很好 2147 | 两梯三 2148 | 在贝壳 2149 | 有点偏高 2150 | 是全款吗 2151 | 几年的 2152 | 给您讲 2153 | 价格贵 2154 | 就在小区 2155 | 打造的 2156 | 确认下 2157 | 在出租中 2158 | 过来看下 2159 | 能见到 2160 | 是绿地 2161 | 在谈价格 2162 | 价格合适 2163 | 能优惠 2164 | 毕竟是 2165 | 去拍照片 2166 | 塔楼的 2167 | 位置也 2168 | 大概需要 2169 | 三栋楼 2170 | 那就只能 2171 | 5万左右 2172 | 是换房 2173 | 3月份 2174 | 22层 2175 | 里小区 2176 | 人民小学 2177 | 装电梯 2178 | 首付预算 2179 | 300米 2180 | 开窗户 2181 | 诚心客户 2182 | 人关注 2183 | 还考虑吗 2184 | 城区的 2185 | 就知道 2186 | 得房率高 2187 | 正在办 2188 | 都能上 2189 | 有密码 2190 | 给您约 2191 | 上午还是 2192 | 太晚了 2193 | 一梯三 2194 | 过来看吗 2195 | 推给您 2196 | 同微信 2197 | 电话吧 2198 | 景华庭 2199 | 东湖中学 2200 | 您看到 2201 | 168万 2202 | 你介绍 2203 | 把钥匙 2204 | 不带家具 2205 | 都不知道 2206 | 北苑的 2207 | 号码吗 2208 | 都有窗户 2209 | 车位比 2210 | 业主自己 2211 | 几个点 2212 | 9层的 2213 | 东南向 2214 | 房主说 2215 | 能买的 2216 | 大概要 2217 | 需要用 2218 | 196万 2219 | 配套都有 2220 | 谈一点 2221 | 的情况和 2222 | 都很熟悉 2223 | 小学对口 2224 | 131平 2225 | 最高贷款 2226 | 96年 2227 | 一共就 2228 | 是拆迁 2229 | 贷款多 2230 | 基础上 2231 | 总高6楼 2232 | 装修保持 2233 | 付全款 2234 | 赠送面积 2235 | 海淀实验 2236 | 6月份 2237 | 给您介绍 2238 | 在附近住 2239 | 顶层的 2240 | 能说一下 2241 | 过两天 2242 | 只有一期 2243 | 物业费1 2244 | 贷款利率 2245 | 我们约 2246 | 您先看看 2247 | 2期的 2248 | 称呼您 2249 | 二套贷 2250 | 是顶层楼 2251 | 你想买 2252 | 能帮我 2253 | 之前聊过 2254 | 用vr带 2255 | 41号楼 2256 | 贷款买 2257 | 一次了 2258 | 不了太多 2259 | 我去谈 2260 | 不太大 2261 | 湖花园的 2262 | 19号楼 2263 | 做工作 2264 | 肯定不是 2265 | 你喜欢 2266 | 96平 2267 | 我给您找 2268 | 10平 2269 | 咱们就 2270 | 及时联系 2271 | 满三年 2272 | 考虑孩子 2273 | 我们肯定 2274 | 贵姓呢 2275 | 得21号 2276 | 比较低 2277 | 还有别的 2278 | 首付大约 2279 | 电话我约 2280 | 园区中间 2281 | 您好久 2282 | 约业主 2283 | 中学位 2284 | 首套三成 2285 | 考虑投资 2286 | 就想要 2287 | 贝壳网 2288 | 朝中庭 2289 | 650万 2290 | 刚才那个 2291 | 四区的 2292 | 发几套 2293 | 楼层也 2294 | 80平 2295 | 二套首付 2296 | 点击查看 2297 | 给您找找 2298 | 公里左右 2299 | 带花园 2300 | 见面聊聊 2301 | 维护的 2302 | 总高34 2303 | 五证齐全 2304 | 280万 2305 | 证满几年 2306 | 来开门 2307 | 湖校区 2308 | 打算卖 2309 | 25楼 2310 | 这段时间 2311 | 117万 2312 | 106万 2313 | 挺方便的 2314 | 次新房 2315 | 这边想找 2316 | 什么优惠 2317 | 都不错 2318 | 低一些 2319 | 88万 2320 | 要约一下 2321 | 14层 2322 | 海国际 2323 | 不是顶层 2324 | 一起去看 2325 | 建成年代 2326 | 我们去 2327 | 得问问 2328 | 要去看 2329 | 30左右 2330 | 都挺好的 2331 | 一个顶楼 2332 | 诚心出售 2333 | 富力城 2334 | 能找到 2335 | 错过了 2336 | 打通了 2337 | 西南朝向 2338 | 见面聊 2339 | 税费少 2340 | 首付2 2341 | 初中初中 2342 | 自己装修 2343 | 一个星期 2344 | 40年的 2345 | 有两个 2346 | 14万 2347 | 没有拍 2348 | 都已经 2349 | 一个电话 2350 | 挨着马路 2351 | 两栋楼 2352 | 也是1 2353 | 时间方便 2354 | 楼层比 2355 | 诚心买房 2356 | 380万 2357 | 你要看 2358 | 7号线 2359 | 700米 2360 | 600米 2361 | 大学附属 2362 | 是几套房 2363 | 天府新区 2364 | 你帮我 2365 | 太低了 2366 | 想问问 2367 | 对口初中 2368 | 250万 2369 | 这边帮您 2370 | 23楼 2371 | 出租装修 2372 | 给我说下 2373 | 就想看看 2374 | 应该会 2375 | 挺高的 2376 | 可以停 2377 | 去看一下 2378 | 包大税 2379 | 你加我 2380 | 几号楼的 2381 | 满五年公 2382 | 年建成 2383 | 是双拼 2384 | 刚刚那套 2385 | 50米 2386 | 房本在手 2387 | 想咨询 2388 | 在哪儿 2389 | 和商贷 2390 | 比较远 2391 | 谈的余地 2392 | 跟您说下 2393 | 3室的 2394 | 新房源 2395 | 是赠送的 2396 | 126万 2397 | 地产政府 2398 | 所以带 2399 | 我说一 2400 | 电话给您 2401 | 大阳台 2402 | 星云苑 2403 | 只能说 2404 | 就住在 2405 | 那咱们 2406 | 东区西区 2407 | 还是比较 2408 | 中再谈 2409 | 也可以当 2410 | 82万 2411 | 245万 2412 | 我们这里 2413 | 我帮你约 2414 | 都没什么 2415 | 业主把 2416 | 实用面积 2417 | 租的话 2418 | 58平米 2419 | 一厅一卫 2420 | 在谈了 2421 | 盐道街 2422 | 一手业主 2423 | 好久了 2424 | 59万 2425 | 第四小学 2426 | 海淀区的 2427 | 五十七中 2428 | 好久满 2429 | 二附中 2430 | 海中学 2431 | 麻烦你 2432 | 户型很 2433 | 已经满了 2434 | 有啥缺点 2435 | 带车库 2436 | 很多人 2437 | 三个小区 2438 | 只有一 2439 | 三号楼 2440 | 万科城 2441 | 考虑新房 2442 | 为您找房 2443 | 还是投资 2444 | 这个盘 2445 | 省一级 2446 | 对口哪个 2447 | 买车位 2448 | 差一些 2449 | 为您推荐 2450 | 不能确定 2451 | 我打给您 2452 | 19万 2453 | 答复您 2454 | 有车位吗 2455 | 上午好 2456 | 赠送吗 2457 | 基本就是 2458 | 个月左右 2459 | 急用钱 2460 | 临马路 2461 | 华小学 2462 | 置业顾问 2463 | 业主当时 2464 | 周边情况 2465 | 小孩上学 2466 | 具体还 2467 | 业主真实 2468 | 绿化率为 2469 | 这边对于 2470 | 5月3号 2471 | 加微信聊 2472 | 是考虑卖 2473 | 另外算 2474 | 有点老 2475 | 总价多少 2476 | 有点偏 2477 | 给您留意 2478 | 一下哈 2479 | 换房子 2480 | 220万 2481 | 尽力帮您 2482 | 只能贷 2483 | 能帮到 2484 | 02年 2485 | 490万 2486 | 个点契税 2487 | 我把房源 2488 | 价格也不 2489 | 还是很 2490 | 为你服务 2491 | 我约业主 2492 | 价格能聊 2493 | 09年的 2494 | 66万 2495 | 客厅带 2496 | 价格太高 2497 | 他说让 2498 | 好像不是 2499 | 是想咨询 2500 | 103万 2501 | 10月 2502 | 中分校 2503 | 可以办 2504 | 您看过嘛 2505 | 在120 2506 | 是哪一 2507 | 经开区 2508 | 找几套 2509 | 29楼 2510 | 非常熟悉 2511 | 你推荐 2512 | 还没有办 2513 | 树德中学 2514 | 帮您找 2515 | 是7楼 2516 | 还有两 2517 | 套同户型 2518 | 价多少 2519 | 局宿舍 2520 | 想了解下 2521 | 去看看吧 2522 | 商住两用 2523 | 没啥问题 2524 | 一手买进 2525 | 没有窗户 2526 | 一般都 2527 | 精装交付 2528 | 领包入住 2529 | 可以买个 2530 | 对流的 2531 | 这样说 2532 | 也不敢 2533 | 7月份 2534 | 还是挺 2535 | 我会尽快 2536 | 硬伤吗 2537 | 刚成交 2538 | 每个月 2539 | 我尽量 2540 | 有停车位 2541 | 昨天有 2542 | 帮您匹配 2543 | 稍等下 2544 | 有啥毛病 2545 | 门店就在 2546 | 套三双卫 2547 | 不会被 2548 | 您先看 2549 | 产证面积 2550 | 40年 2551 | 94万 2552 | 没有影响 2553 | 在小区里 2554 | 我们说的 2555 | 及时回复 2556 | 38万 2557 | 固定车位 2558 | 正在建 2559 | 带地下室 2560 | 22万 2561 | 有啥问题 2562 | 带走吗 2563 | 可能会 2564 | 3分钟 2565 | 108万 2566 | 相对于 2567 | 首套首付 2568 | 住过人 2569 | 房本面积 2570 | 我再约 2571 | 高新第三 2572 | 不是同 2573 | 几期啊 2574 | 在卖掉 2575 | 准备出售 2576 | 72万 2577 | 让他们 2578 | 十三中学 2579 | 附近配套 2580 | 要看看嘛 2581 | 随便停 2582 | 不收费 2583 | 另外一套 2584 | 得多少 2585 | 也没用 2586 | 不包括 2587 | 用率高 2588 | 要考核 2589 | 总高7层 2590 | 学位呢 2591 | 可以再谈 2592 | 考虑多大 2593 | 有任何 2594 | 管理好 2595 | 位置就在 2596 | 我说话吗 2597 | 苑小学 2598 | 离地铁近 2599 | 一点到两 2600 | 没有啥 2601 | 告诉我们 2602 | 比较稀缺 2603 | 小区外 2604 | 生活方便 2605 | 15层 2606 | 二环内 2607 | 华师附小 2608 | 算首套 2609 | 您想买个 2610 | 没有固定 2611 | 102万 2612 | 是简装 2613 | 再卖的 2614 | 靠高架 2615 | 我约约 2616 | 有车库 2617 | 需要约 2618 | 33平 2619 | 含车位吗 2620 | 39万 2621 | 不过二 2622 | 一卫的 2623 | 中学对口 2624 | 聊价格 2625 | 首付50 2626 | 我详细 2627 | 是自如 2628 | 啥价格 2629 | 中心位置 2630 | 都可以买 2631 | 要另外 2632 | 外地出差 2633 | 新上架 2634 | 好久满二 2635 | 下午1 2636 | 40多万 2637 | 去看下 2638 | 28号楼 2639 | 然后再 2640 | 真没有 2641 | 本来就 2642 | 办下来 2643 | 我发送 2644 | 纯商贷 2645 | 负一楼 2646 | 发你看看 2647 | 听不到 2648 | 32楼 2649 | 如果买 2650 | 140平 2651 | 我给您约 2652 | 也很不错 2653 | 像这样的 2654 | 有点遮挡 2655 | 整个小区 2656 | 推给你 2657 | 在深圳 2658 | 都有哪些 2659 | 东南朝向 2660 | 全款买的 2661 | 给我说 2662 | 就没事 2663 | 就是想 2664 | 这种小 2665 | 我们同事 2666 | 32层 2667 | 在西安 2668 | 先看看 2669 | 不含车位 2670 | 580万 2671 | 还不是很 2672 | 环境一般 2673 | 下班后 2674 | 一直都是 2675 | 给你回话 2676 | 新城小学 2677 | 均价1 2678 | 给我说说 2679 | 还有其它 2680 | 25万 2681 | 2年了 2682 | 看完房 2683 | 11栋的 2684 | 180万 2685 | 52万 2686 | 红星海 2687 | 可以让 2688 | 业主心里 2689 | 还不知道 2690 | 四房吗 2691 | 很齐全 2692 | 有差距 2693 | 明天休息 2694 | 在店里 2695 | 帮您看 2696 | 通透吗 2697 | 定制的 2698 | 4号楼的 2699 | 本证的 2700 | 带地下 2701 | 能买到 2702 | 十中学 2703 | 装修很好 2704 | 不及时 2705 | 上去了 2706 | 才交房 2707 | 得行不 2708 | 145万 2709 | 4号楼 2710 | 更优惠 2711 | 如果您对 2712 | 我找几套 2713 | 银行不 2714 | 小区内 2715 | 配套也 2716 | 要卖了 2717 | 光线怎样 2718 | 多久满2 2719 | 急售房源 2720 | 契税5 2721 | 楼层很好 2722 | 南北朝向 2723 | 325万 2724 | 自己装 2725 | 同样的 2726 | 您打开 2727 | 客厅朝南 2728 | 详细了解 2729 | 采光还行 2730 | 来看看吧 2731 | 明年年 2732 | 132万 2733 | 435万 2734 | 5分钟到 2735 | 老校区 2736 | 能留个 2737 | -------------------------------------------------------------------------------- /img/bottom-embedding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xv44586/ccf_2020_qa_match/93e269f0bd5f1a060db2bb6d8e6a92b73bb7b2d4/img/bottom-embedding.png -------------------------------------------------------------------------------- /img/concat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xv44586/ccf_2020_qa_match/93e269f0bd5f1a060db2bb6d8e6a92b73bb7b2d4/img/concat.png -------------------------------------------------------------------------------- /img/pair.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xv44586/ccf_2020_qa_match/93e269f0bd5f1a060db2bb6d8e6a92b73bb7b2d4/img/pair.png -------------------------------------------------------------------------------- /img/pet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xv44586/ccf_2020_qa_match/93e269f0bd5f1a060db2bb6d8e6a92b73bb7b2d4/img/pet.png -------------------------------------------------------------------------------- /img/point.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xv44586/ccf_2020_qa_match/93e269f0bd5f1a060db2bb6d8e6a92b73bb7b2d4/img/point.png -------------------------------------------------------------------------------- /img/post-training.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xv44586/ccf_2020_qa_match/93e269f0bd5f1a060db2bb6d8e6a92b73bb7b2d4/img/post-training.png -------------------------------------------------------------------------------- /img/sc-loss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xv44586/ccf_2020_qa_match/93e269f0bd5f1a060db2bb6d8e6a92b73bb7b2d4/img/sc-loss.png -------------------------------------------------------------------------------- /img/sc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xv44586/ccf_2020_qa_match/93e269f0bd5f1a060db2bb6d8e6a92b73bb7b2d4/img/sc.png -------------------------------------------------------------------------------- /img/ssc-loss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xv44586/ccf_2020_qa_match/93e269f0bd5f1a060db2bb6d8e6a92b73bb7b2d4/img/ssc-loss.png -------------------------------------------------------------------------------- /img/ssc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xv44586/ccf_2020_qa_match/93e269f0bd5f1a060db2bb6d8e6a92b73bb7b2d4/img/ssc.png -------------------------------------------------------------------------------- /img/summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xv44586/ccf_2020_qa_match/93e269f0bd5f1a060db2bb6d8e6a92b73bb7b2d4/img/summary.png -------------------------------------------------------------------------------- /img/top-embedding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xv44586/ccf_2020_qa_match/93e269f0bd5f1a060db2bb6d8e6a92b73bb7b2d4/img/top-embedding.png -------------------------------------------------------------------------------- /new_words_mining.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Date : 2020/12/8 3 | # @Author : mingming.xu 4 | # @Email : xv44586@gmail.com 5 | # @File : pmi.py 6 | """ 7 | 根据PMI 挖掘新词与短语 8 | ref: [最小熵原理(二):“当机立断”之词库构建](https://kexue.fm/archives/5476) 9 | """ 10 | import os 11 | from nlp_zero import * 12 | import jieba 13 | 14 | jieba.initialize() 15 | path = '/home/mingming.xu/datasets/NLP/ccf_qa_match/' 16 | 17 | 18 | def load_data(train_test='train'): 19 | D = {} 20 | with open(os.path.join(path, train_test, train_test + '.query.tsv')) as f: 21 | for l in f: 22 | span = l.strip().split('\t') 23 | D[span[0]] = {'query': span[1], 'reply': []} 24 | 25 | with open(os.path.join(path, train_test, train_test + '.reply.tsv')) as f: 26 | for l in f: 27 | span = l.strip().split('\t') 28 | if len(span) == 4: 29 | q_id, r_id, r, label = span 30 | else: 31 | label = None 32 | q_id, r_id, r = span 33 | D[q_id]['reply'].append([r_id, r, label]) 34 | d = [] 35 | for k, v in D.items(): 36 | q = v['query'] 37 | reply = v['reply'] 38 | 39 | cor = [q] + [r[1] for r in reply] 40 | d.append(''.join(cor)) 41 | 42 | return d 43 | 44 | 45 | train_data = load_data('train') 46 | test_data = load_data('test') 47 | 48 | 49 | class G(object): 50 | def __iter__(self): 51 | for i in train_data + test_data: 52 | yield i 53 | 54 | 55 | f = Word_Finder(min_proba=1e-5) 56 | f.train(G()) 57 | f.find(G()) 58 | 59 | # 长度为2~5 且不在jieba 词典内的词 60 | new_words = [w for w, _ in f.words.items() if len(w) > 2 and len(w) < 5 and len(jieba.lcut(w, HMM=False)) > 1] 61 | 62 | with open('new_dict.txt', 'w') as f: 63 | f.write('\n'.join(new_words)) 64 | -------------------------------------------------------------------------------- /pair-adversarial-train.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Date : 2021/1/18 3 | # @Author : mingming.xu 4 | # @Email : xv44586@gmail.com 5 | # @File : pair-adversarial-train.py 6 | import os 7 | from tqdm import tqdm 8 | import numpy as np 9 | 10 | from toolkit4nlp.utils import * 11 | from toolkit4nlp.models import * 12 | from toolkit4nlp.layers import * 13 | from toolkit4nlp.optimizers import * 14 | from toolkit4nlp.tokenizers import Tokenizer 15 | from toolkit4nlp.backend import * 16 | 17 | batch_size = 16 18 | maxlen = 280 19 | epochs = 10 20 | lr = 1e-5 21 | 22 | # bert配置 23 | config_path = '/home/mingming.xu/pretrain/NLP/nezha_base_wwm/bert_config.json' 24 | checkpoint_path = '/home/mingming.xu/pretrain/NLP/nezha_base_wwm/model.ckpt' 25 | dict_path = '/home/mingming.xu/pretrain/NLP/nezha_base_wwm//vocab.txt' 26 | # 建立分词器 27 | tokenizer = Tokenizer(dict_path, do_lower_case=True) 28 | 29 | path = '/home/mingming.xu/datasets/NLP/ccf_qa_match/' 30 | 31 | 32 | def load_data(train_test='train'): 33 | D = {} 34 | with open(os.path.join(path, train_test, train_test + '.query.tsv')) as f: 35 | for l in f: 36 | span = l.strip().split('\t') 37 | D[span[0]] = {'query': span[1], 'reply': []} 38 | 39 | with open(os.path.join(path, train_test, train_test + '.reply.tsv')) as f: 40 | for l in f: 41 | span = l.strip().split('\t') 42 | if len(span) == 4: 43 | q_id, r_id, r, label = span 44 | else: 45 | label = None 46 | q_id, r_id, r = span 47 | D[q_id]['reply'].append([r_id, r, label]) 48 | d = [] 49 | for k, v in D.items(): 50 | q_id = k 51 | q = v['query'] 52 | reply = v['reply'] 53 | 54 | for r in reply: 55 | r_id, rc, label = r 56 | 57 | d.append([q_id, q, r_id, rc, label]) 58 | return d 59 | 60 | 61 | train_data = load_data('train') 62 | test_data = load_data('test') 63 | 64 | 65 | class data_generator(DataGenerator): 66 | def __iter__(self, shuffle=False): 67 | batch_token_ids, batch_segment_ids, batch_labels = [], [], [] 68 | for is_end, (q_id, q, r_id, r, label) in self.get_sample(shuffle): 69 | label = int(label) if label is not None else None 70 | 71 | token_ids, segment_ids = tokenizer.encode(q, r, maxlen=256) 72 | 73 | batch_token_ids.append(token_ids) 74 | batch_segment_ids.append(segment_ids) 75 | batch_labels.append([label]) 76 | 77 | if is_end or len(batch_token_ids) == self.batch_size: 78 | batch_token_ids = pad_sequences(batch_token_ids) 79 | batch_segment_ids = pad_sequences(batch_segment_ids) 80 | batch_labels = pad_sequences(batch_labels) 81 | 82 | yield [batch_token_ids, batch_segment_ids], batch_labels 83 | 84 | batch_token_ids, batch_segment_ids, batch_labels = [], [], [] 85 | 86 | 87 | # shuffle 88 | np.random.shuffle(train_data) 89 | n = int(len(train_data) * 0.8) 90 | train_generator = data_generator(train_data[:n], batch_size) 91 | valid_generator = data_generator(train_data[n:], batch_size) 92 | test_generator = data_generator(test_data, batch_size) 93 | 94 | # 加载预训练模型 95 | bert = build_transformer_model( 96 | config_path=config_path, 97 | checkpoint_path=checkpoint_path, 98 | # model='bert', # 加载bert/Roberta/ernie 99 | # model='electra', # 加载electra 100 | model='nezha', # 加载NEZHA 101 | ) 102 | output = bert.output 103 | 104 | output = Lambda(lambda x: x[:, 0])(output) 105 | output = Dense(1, activation='sigmoid')(output) 106 | 107 | model = keras.models.Model(bert.input, output) 108 | model.summary() 109 | 110 | model.compile( 111 | loss=K.binary_crossentropy, 112 | optimizer=Adam(2e-5), 113 | metrics=['accuracy'], 114 | ) 115 | 116 | 117 | def adversarial_training(model, embedding_name, epsilon=1): 118 | """给模型添加对抗训练 119 | 其中model是需要添加对抗训练的keras模型,embedding_name 120 | 则是model里边Embedding层的名字。要在模型compile之后使用。 121 | """ 122 | if model.train_function is None: # 如果还没有训练函数 123 | model._make_train_function() # 手动make 124 | old_train_function = model.train_function # 备份旧的训练函数 125 | 126 | # 查找Embedding层 127 | for output in model.outputs: 128 | embedding_layer = search_layer(output, embedding_name) 129 | if embedding_layer is not None: 130 | break 131 | if embedding_layer is None: 132 | raise Exception('Embedding layer not found') 133 | 134 | # 求Embedding梯度 135 | embeddings = embedding_layer.embeddings # Embedding矩阵 136 | gradients = K.gradients(model.total_loss, [embeddings]) # Embedding梯度 137 | gradients = K.zeros_like(embeddings) + gradients[0] # 转为dense tensor 138 | 139 | # 封装为函数 140 | inputs = ( 141 | model._feed_inputs + model._feed_targets + model._feed_sample_weights 142 | ) # 所有输入层 143 | embedding_gradients = K.function( 144 | inputs=inputs, 145 | outputs=[gradients], 146 | name='embedding_gradients', 147 | ) # 封装为函数 148 | 149 | def train_function(inputs): # 重新定义训练函数 150 | grads = embedding_gradients(inputs)[0] # Embedding梯度 151 | delta = epsilon * grads / (np.sqrt((grads ** 2).sum()) + 1e-8) # 计算扰动 152 | K.set_value(embeddings, K.eval(embeddings) + delta) # 注入扰动 153 | outputs = old_train_function(inputs) # 梯度下降 154 | K.set_value(embeddings, K.eval(embeddings) - delta) # 删除扰动 155 | return outputs 156 | 157 | model.train_function = train_function # 覆盖原训练函数 158 | 159 | 160 | # 写好函数后,启用对抗训练只需要一行代码 161 | adversarial_training(model, 'Embedding-Token', 0.5) 162 | 163 | 164 | def evaluate(data): 165 | P, R, TP = 0., 0., 0. 166 | for x_true, y_true in tqdm(data): 167 | y_pred = model.predict(x_true)[:, 0] 168 | y_pred = np.round(y_pred) 169 | y_true = y_true[:, 0] 170 | 171 | R += y_pred.sum() 172 | P += y_true.sum() 173 | TP += ((y_pred + y_true) > 1).sum() 174 | 175 | print(P, R, TP) 176 | pre = TP / R 177 | rec = TP / P 178 | 179 | return 2 * (pre * rec) / (pre + rec) 180 | 181 | 182 | class Evaluator(keras.callbacks.Callback): 183 | """评估与保存 184 | """ 185 | 186 | def __init__(self): 187 | self.best_val_f1 = 0. 188 | 189 | def on_epoch_end(self, epoch, logs=None): 190 | val_f1 = evaluate(valid_generator) 191 | if val_f1 > self.best_val_f1: 192 | self.best_val_f1 = val_f1 193 | model.save_weights('best_parimatch_model.weights') 194 | print( 195 | u'val_f1: %.5f, best_val_f1: %.5f\n' % 196 | (val_f1, self.best_val_f1) 197 | ) 198 | 199 | 200 | def predict_to_file(path='pair_submission.tsv', data=test_generator): 201 | preds = [] 202 | for x, _ in tqdm(test_generator): 203 | pred = model.predict(x)[:, 0] 204 | pred = np.round(pred) 205 | pred = pred.astype(int) 206 | preds.extend(pred) 207 | 208 | ret = [] 209 | for d, p in zip(test_data, preds): 210 | q_id, _, r_id, _, _ = d 211 | ret.append([str(q_id), str(r_id), str(p)]) 212 | 213 | with open(path, 'w', encoding='utf8') as f: 214 | for l in ret: 215 | f.write('\t'.join(l) + '\n') 216 | 217 | 218 | if __name__ == '__main__': 219 | evaluator = Evaluator() 220 | model.fit_generator( 221 | train_generator.generator(), 222 | steps_per_epoch=len(train_generator), 223 | epochs=5, 224 | callbacks=[evaluator], 225 | ) 226 | 227 | # predict test and write to file 228 | model.load_weights('best_parimatch_model.weights') 229 | predict_to_file() -------------------------------------------------------------------------------- /pair-data-augment-contrastive-learning.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Date : 2020/12/4 3 | # @Author : mingming.xu 4 | # @Email : xv44586@gmail.com 5 | # @File : pair-data-augment-constrastive-learning.py 6 | """ 7 | 借鉴无监督中借助数据增强来做对比学习,采用query-reply 对为基本样本形式,通过互换query/reply 的位置构造新样本,做对比学习 8 | 9 | 线下结果:提升不明显 10 | """ 11 | import os 12 | import numpy as np 13 | from tqdm import tqdm 14 | 15 | from toolkit4nlp.utils import * 16 | from toolkit4nlp.models import * 17 | from toolkit4nlp.tokenizers import * 18 | from toolkit4nlp.backend import * 19 | from toolkit4nlp.layers import * 20 | from toolkit4nlp.optimizers import * 21 | 22 | path = '/home/mingming.xu/datasets/NLP/ccf_qa_match/' 23 | 24 | maxlen = 128 25 | batch_size = 32 26 | epochs = 10 27 | 28 | config_path = '/home/mingming.xu/pretrain/NLP/nezha_base_wwm/bert_config.json' 29 | checkpoint_path = '/home/mingming.xu/pretrain/NLP/nezha_base_wwm/model.ckpt' 30 | dict_path = '/home/mingming.xu/pretrain/NLP/nezha_base_wwm/vocab.txt' 31 | 32 | # 建立分词器 33 | token_dict, keep_tokens = load_vocab(dict_path, 34 | simplified=True, 35 | startswith=['[PAD]', '[UNK]', '[MASK]', '[CLS]', '[SEP]']) 36 | 37 | tokenizer = Tokenizer(token_dict, do_lower_case=True) 38 | 39 | 40 | def load_data(train_test='train'): 41 | D = {} 42 | with open(os.path.join(path, train_test, train_test + '.query.tsv')) as f: 43 | for l in f: 44 | span = l.strip().split('\t') 45 | D[span[0]] = {'query': span[1], 'reply': []} 46 | 47 | with open(os.path.join(path, train_test, train_test + '.reply.tsv')) as f: 48 | for l in f: 49 | span = l.strip().split('\t') 50 | if len(span) == 4: 51 | q_id, r_id, r, label = span 52 | else: 53 | label = None 54 | q_id, r_id, r = span 55 | D[q_id]['reply'].append([r_id, r, label]) 56 | d = [] 57 | for k, v in D.items(): 58 | q_id = k 59 | q = v['query'] 60 | reply = v['reply'] 61 | 62 | for r in reply: 63 | r_id, rc, label = r 64 | 65 | d.append([q_id, q, r_id, rc, label]) 66 | return d 67 | 68 | 69 | train_data = load_data('train') 70 | test_data = load_data('test') 71 | 72 | 73 | def can_padding(token_id): 74 | if token_id in (tokenizer._token_mask_id, tokenizer._token_end_id, tokenizer._token_start_id): 75 | return False 76 | return True 77 | 78 | 79 | class data_generator(DataGenerator): 80 | def random_padding(self, token_ids): 81 | rands = np.random.random(len(token_ids)) 82 | new_tokens = [] 83 | for p, token in zip(rands, token_ids): 84 | if p < 0.1 and can_padding(token): 85 | new_tokens.append(tokenizer._token_pad_id) 86 | else: 87 | new_tokens.append(token) 88 | return new_tokens 89 | 90 | def __iter__(self, shuffle=False): 91 | batch_token_ids, batch_segment_ids, batch_labels = [], [], [] 92 | for is_end, (q_id, q, r_id, r, label) in self.get_sample(shuffle): 93 | label = float(label) if label is not None else None 94 | 95 | if shuffle: 96 | token_ids_1, segment_ids_1 = tokenizer.encode(q, r, maxlen=maxlen) 97 | token_ids_1 = self.random_padding(token_ids_1) 98 | token_ids_2, segment_ids_2 = tokenizer.encode(r, q, maxlen=maxlen) 99 | token_ids_2 = self.random_padding(token_ids_2) 100 | batch_token_ids.extend([token_ids_1, token_ids_2]) 101 | batch_segment_ids.extend([segment_ids_1, segment_ids_2]) 102 | batch_labels.extend([[label], [label]]) 103 | 104 | else: 105 | token_ids, segment_ids = tokenizer.encode(q, r, maxlen=maxlen) 106 | batch_token_ids.append(token_ids) 107 | batch_segment_ids.append(segment_ids) 108 | batch_labels.append([label]) 109 | 110 | if is_end or len(batch_token_ids) == self.batch_size * 2: 111 | batch_token_ids = pad_sequences(batch_token_ids) 112 | batch_segment_ids = pad_sequences(batch_segment_ids) 113 | batch_labels = pad_sequences(batch_labels) 114 | 115 | yield [batch_token_ids, batch_segment_ids], batch_labels 116 | 117 | batch_token_ids, batch_segment_ids, batch_labels = [], [], [] 118 | 119 | 120 | # shuffle 121 | np.random.shuffle(train_data) 122 | n = int(len(train_data) * 0.8) 123 | valid_data, train_data = train_data[n:], train_data[:n] 124 | train_generator = data_generator(data=train_data, batch_size=batch_size) 125 | valid_generator = data_generator(data=valid_data, batch_size=batch_size) 126 | test_generator = data_generator(data=test_data, batch_size=batch_size) 127 | print(len(train_data), len(valid_data)) 128 | 129 | 130 | class ContrastiveLoss(Loss): 131 | """loss: 相似度的交叉熵。 132 | """ 133 | 134 | def __init__(self, alpha=1., T=1., **kwargs): 135 | super(ContrastiveLoss, self).__init__(**kwargs) 136 | self.alpha = alpha # 权重weight 137 | self.T = T # 平滑温度 138 | 139 | def compute_loss(self, inputs, mask=None): 140 | loss = self.compute_loss_of_similarity(inputs, mask) 141 | loss = loss * self.alpha 142 | self.add_metric(loss, name='similarity_loss') 143 | return loss 144 | 145 | def compute_loss_of_similarity(self, inputs, mask=None): 146 | y_pred = inputs 147 | y_true = self.get_labels_of_similarity(y_pred) # 构建标签 148 | y_pred = K.l2_normalize(y_pred, axis=1) # 句向量归一化 149 | similarities = K.dot(y_pred, K.transpose(y_pred)) # 相似度矩阵 150 | similarities = similarities - K.eye(K.shape(y_pred)[0]) * 1e12 # 排除对角线 151 | similarities = similarities / self.T # scale 152 | loss = K.categorical_crossentropy( 153 | y_true, similarities, from_logits=True 154 | ) 155 | return loss 156 | 157 | def get_labels_of_similarity(self, y_pred): 158 | idxs = K.arange(0, K.shape(y_pred)[0]) 159 | idxs_1 = idxs[None, :] 160 | idxs_2 = (idxs + 1 - idxs % 2 * 2)[:, None] 161 | labels = K.equal(idxs_1, idxs_2) 162 | labels = K.cast(labels, K.floatx()) 163 | return labels 164 | 165 | 166 | # 加载预训练模型 167 | bert = build_transformer_model( 168 | config_path=config_path, 169 | checkpoint_path=checkpoint_path, 170 | model='nezha', 171 | keep_tokens=keep_tokens, 172 | num_hidden_layers=10, # 173 | ) 174 | output = Lambda(lambda x: x[:, 0])(bert.output) 175 | 176 | cons_output = ContrastiveLoss(alpha=1, T=0.1)(output) 177 | 178 | output = Dropout(0.1)(output) 179 | output = Dense(2)(output) 180 | clf_output = Activation('softmax', name='clf')(output) 181 | 182 | model = keras.models.Model(bert.input, clf_output) 183 | model.summary() 184 | 185 | train_model = keras.models.Model(bert.input, [cons_output, clf_output]) 186 | optimizer = extend_with_weight_decay(Adam) 187 | optimizer = extend_with_piecewise_linear_lr(optimizer) 188 | opt = optimizer(learning_rate=1e-5, weight_decay_rate=0.1, exclude_from_weight_decay=['Norm', 'bias'], 189 | lr_schedule={int(len(train_generator) * 0.1 * epochs): 1, len(train_generator) * epochs: 0} 190 | ) 191 | 192 | train_model.compile( 193 | loss=[None, 'sparse_categorical_crossentropy'], 194 | optimizer=opt, 195 | ) 196 | 197 | 198 | def evaluate(data): 199 | P, R, TP = 0., 0., 0. 200 | for x_true, y_true in tqdm(data): 201 | y_pred = model.predict(x_true).argmax(axis=1) 202 | # y_pred = np.round(y_pred) 203 | y_true = y_true[:, 0] 204 | 205 | R += y_pred.sum() 206 | P += y_true.sum() 207 | TP += ((y_pred + y_true) > 1).sum() 208 | 209 | print(P, R, TP) 210 | pre = TP / R 211 | rec = TP / P 212 | 213 | return 2 * (pre * rec) / (pre + rec) 214 | 215 | 216 | class Evaluator(keras.callbacks.Callback): 217 | """评估与保存 218 | """ 219 | 220 | def __init__(self, save_path): 221 | self.best_val_f1 = 0. 222 | self.save_path = save_path 223 | 224 | def on_epoch_end(self, epoch, logs=None): 225 | val_f1 = evaluate(valid_generator) 226 | if val_f1 > self.best_val_f1: 227 | self.best_val_f1 = val_f1 228 | model.save_weights(self.save_path) 229 | print( 230 | u'val_f1: %.5f, best_val_f1: %.5f\n' % 231 | (val_f1, self.best_val_f1) 232 | ) 233 | 234 | 235 | def predict_to_file(path='pair_submission.tsv', data=test_generator): 236 | preds = [] 237 | for x, _ in tqdm(test_generator): 238 | pred = model.predict(x)[:, 0] 239 | pred = np.round(pred) 240 | pred = pred.astype(int) 241 | preds.extend(pred) 242 | 243 | ret = [] 244 | for d, p in zip(test_data, preds): 245 | q_id, _, r_id, _, _ = d 246 | ret.append([str(q_id), str(r_id), str(p)]) 247 | 248 | with open(path, 'w', encoding='utf8') as f: 249 | for l in ret: 250 | f.write('\t'.join(l) + '\n') 251 | 252 | 253 | if __name__ == '__main__': 254 | save_path = 'best_parimatch_ag_cl_model.weights' 255 | evaluator = Evaluator(save_path) 256 | train_model.fit_generator( 257 | train_generator.generator(), 258 | steps_per_epoch=len(train_generator), 259 | epochs=epochs, 260 | callbacks=[evaluator], 261 | ) 262 | 263 | model.load_weights(save_path) 264 | predict_to_file('pair_ag_cl_submission.tsv') 265 | -------------------------------------------------------------------------------- /pair-external-embedding.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Date : 2021/1/18 3 | # @Author : mingming.xu 4 | # @Email : xv44586@gmail.com 5 | # @File : pair-external-embedding.py 6 | import os 7 | import numpy as np 8 | 9 | import numpy as np 10 | from tqdm import tqdm 11 | 12 | import gensim 13 | 14 | from toolkit4nlp.utils import * 15 | from toolkit4nlp.models import * 16 | from toolkit4nlp.tokenizers import * 17 | from toolkit4nlp.backend import * 18 | from toolkit4nlp.layers import * 19 | from toolkit4nlp.optimizers import * 20 | 21 | path = '/home/mingming.xu/datasets/NLP/ccf_qa_match/' 22 | 23 | p = os.path.join(path, 'train', 'train.query.tsv') 24 | 25 | 26 | def load_data(train_test='train'): 27 | D = {} 28 | with open(os.path.join(path, train_test, train_test + '.query.tsv')) as f: 29 | for l in f: 30 | span = l.strip().split('\t') 31 | D[span[0]] = {'query': span[1], 'reply': []} 32 | 33 | with open(os.path.join(path, train_test, train_test + '.reply.tsv')) as f: 34 | for l in f: 35 | span = l.strip().split('\t') 36 | if len(span) == 4: 37 | q_id, r_id, r, label = span 38 | else: 39 | label = None 40 | q_id, r_id, r = span 41 | D[q_id]['reply'].append([r_id, r, label]) 42 | d = [] 43 | for k, v in D.items(): 44 | q_id = k 45 | q = v['query'] 46 | reply = v['reply'] 47 | 48 | c = [] 49 | l = [] 50 | for r in reply: 51 | r_id, rc, label = r 52 | 53 | d.append([q_id, q, r_id, rc, label]) 54 | return d 55 | 56 | 57 | train_data = load_data('train') 58 | test_data = load_data('test') 59 | 60 | maxlen = 128 61 | batch_size = 16 62 | epochs = 4 63 | # bert配置 64 | # config_path = '/home/mingming.xu/pretrain/NLP/chinese_roberta_wwm_ext_L-12_H-768_A-12/bert_config.json' 65 | # checkpoint_path = '/home/mingming.xu/pretrain/NLP/chinese_roberta_wwm_ext_L-12_H-768_A-12/bert_model.ckpt' 66 | # dict_path = '/home/mingming.xu/pretrain/NLP/chinese_roberta_wwm_ext_L-12_H-768_A-12/vocab.txt' 67 | 68 | config_path = '/home/mingming.xu/pretrain/NLP/nezha_base_wwm/bert_config.json' 69 | # checkpoint_path = '/home/mingming.xu/pretrain/NLP/nezha_base_wwm/model.ckpt' 70 | dict_path = '/home/mingming.xu/pretrain/NLP/nezha_base_wwm/vocab.txt' 71 | checkpoint_path = './pet_checkpoint/pet_1.model' 72 | 73 | # 建立分词器 74 | tokenizer = Tokenizer(dict_path, do_lower_case=True) 75 | 76 | 77 | class data_generator(DataGenerator): 78 | def encode_reply(self, reply_list, label_list): 79 | tokens, segments = [], [] 80 | mid, r = reply_list[:-1], reply_list[-1] 81 | mid_l, l = label_list[:-1], label_list[-1] 82 | 83 | # 过滤之前reply 中的 1,防止影响判断 84 | r_list = [] 85 | for lb, rp in zip(mid_l, mid): 86 | if lb == 0: 87 | r_list.append(rp) 88 | else: 89 | p = np.random.random() 90 | if p > 0.: 91 | r_list.append(rp) 92 | 93 | # 打乱对话顺序 94 | np.random.shuffle(r_list) 95 | for rp in r_list: 96 | token = tokenizer.encode(rp)[0][1:] 97 | segment = [0] * len(token) 98 | tokens += token 99 | segments += segment 100 | 101 | r_tokens = tokenizer.encode(r)[0][1:] 102 | r_segments = [1] * len(r_tokens) 103 | tokens += r_tokens 104 | segments += r_segments 105 | return tokens, segments 106 | 107 | def __iter__(self, shuffle=False): 108 | batch_token_ids, batch_segment_ids, batch_labels = [], [], [] 109 | for is_end, (q_id, q, r_id, r, label) in self.get_sample(shuffle): 110 | # print(q_id, q, r_id, r, label) 111 | label = int(label) if label is not None else None 112 | 113 | token_ids, segment_ids = tokenizer.encode(q, r, maxlen=256) 114 | 115 | batch_token_ids.append(token_ids) 116 | batch_segment_ids.append(segment_ids) 117 | batch_labels.append([label]) 118 | 119 | if is_end or len(batch_token_ids) == self.batch_size: 120 | batch_token_ids = pad_sequences(batch_token_ids) 121 | batch_segment_ids = pad_sequences(batch_segment_ids) 122 | batch_labels = pad_sequences(batch_labels) 123 | 124 | yield [batch_token_ids, batch_segment_ids], batch_labels 125 | 126 | batch_token_ids, batch_segment_ids, batch_labels = [], [], [] 127 | 128 | 129 | # shuffle 130 | np.random.shuffle(train_data) 131 | n = int(len(train_data) * 0.9) 132 | train_generator = data_generator(train_data[:n], batch_size) 133 | valid_generator = data_generator(train_data[n:], batch_size) 134 | test_generator = data_generator(test_data, batch_size) 135 | 136 | 137 | class ConcatSeq2Vec(Layer): 138 | def __init__(self, **kwargs): 139 | super(ConcatSeq2Vec, self).__init__(**kwargs) 140 | 141 | def build(self, input_shape): 142 | super(ConcatSeq2Vec, self).build(input_shape) 143 | 144 | def call(self, x): 145 | seq, vec = x 146 | vec = K.expand_dims(vec, 1) 147 | vec = K.tile(vec, [1, K.shape(seq)[1], 1]) 148 | return K.concatenate([seq, vec], 2) 149 | 150 | def compute_mask(self, inputs, mask): 151 | return mask[0] 152 | 153 | def compute_output_shape(self, input_shape): 154 | return input_shape[0][:-1] + (input_shape[0][-1] + input_shape[1][-1],) 155 | 156 | 157 | # load w2v 158 | w2v = gensim.models.word2vec.Word2Vec.load('qa_100.w2v') 159 | vocab_list = [(k, w2v.wv[k]) for k, _ in w2v.wv.vocab.items()] 160 | embeddings_matrix = np.zeros((tokenizer._vocab_size, 100)) 161 | for (char, vec) in vocab_list: 162 | embeddings_matrix[tokenizer.encode(char)[0][1:-1]] = vec 163 | 164 | 165 | class AdaEmbedding(Embedding): 166 | # 带有可调节学习率的embedding层 167 | def __init__(self, lr_multiplier=1, **kwargs): 168 | super(AdaEmbedding, self).__init__(**kwargs) 169 | self.lr_multiplier = lr_multiplier 170 | 171 | def build(self, input_shape): 172 | self._embeddings = self.add_weight( 173 | shape=(self.input_dim, self.output_dim), 174 | initializer=self.embeddings_initializer, 175 | name='embeddings', 176 | regularizer=self.embeddings_regularizer, 177 | constraint=self.embeddings_constraint, 178 | dtype=self.dtype) 179 | 180 | self.built = True 181 | 182 | if self.lr_multiplier != 1: 183 | K.set_value(self._embeddings, K.eval(self._embeddings) / self.lr_multiplier) 184 | 185 | @property 186 | def embeddings(self): 187 | if self.lr_multiplier != 1: 188 | return self._embeddings * self.lr_multiplier 189 | return self._embeddings 190 | 191 | def call(self, inputs): 192 | return super(AdaEmbedding, self).call(inputs) 193 | 194 | 195 | # embedding 层融合 196 | # bert = build_transformer_model( 197 | # config_path=config_path, 198 | # checkpoint_path=checkpoint_path, 199 | # model='nezha', 200 | # external_embedding_size=100, 201 | # external_embedding_weights=embeddings_matrix # 融入的embedding 202 | # ) 203 | 204 | # transformer output层融合 205 | bert = build_transformer_model( 206 | config_path=config_path, 207 | checkpoint_path=checkpoint_path, 208 | model='nezha', 209 | # external_embedding_size=100, 210 | # external_embedding_weights=embeddings_matrix 211 | ) 212 | output = bert.output 213 | 214 | token_input = bert.inputs[0] 215 | ada_embedding = AdaEmbedding(input_dim=tokenizer._vocab_size, 216 | name='Embedding-External', 217 | output_dim=100, 218 | weights=[embeddings_matrix], 219 | mask_zero=True, 220 | lr_multiplier=2) 221 | external_embedding = ada_embedding(token_input) 222 | x = Concatenate(axis=-1)([output, external_embedding]) 223 | output = Lambda(lambda x: x[:, 0])(output) 224 | # output = Dropout(0.1)(output) 225 | output = Dense(1, activation='sigmoid')(output) 226 | model = keras.models.Model(bert.input, output) 227 | model.summary() 228 | 229 | optimizer = extend_with_weight_decay(Adam) 230 | optimizer = extend_with_piecewise_linear_lr(optimizer) 231 | 232 | opt = optimizer(learning_rate=1e-5, 233 | weight_decay_rate=0.05, exclude_from_weight_decay=['Norm', 'bias'], 234 | lr_schedule={int(len(train_generator) * epochs * 0.1): 1, len(train_generator) * epochs: 0}) 235 | 236 | model.compile( 237 | # loss=binary_focal_loss(0.25, 12), 238 | loss=K.binary_crossentropy, 239 | optimizer=opt, 240 | metrics=['accuracy'], 241 | ) 242 | 243 | 244 | def evaluate(data): 245 | P, R, TP = 0., 0., 0. 246 | for x_true, y_true in tqdm(data): 247 | y_pred = model.predict(x_true)[:, 0] 248 | y_pred = np.round(y_pred) 249 | y_true = y_true[:, 0] 250 | 251 | R += y_pred.sum() 252 | P += y_true.sum() 253 | TP += ((y_pred + y_true) > 1).sum() 254 | 255 | print(P, R, TP) 256 | pre = TP / R 257 | rec = TP / P 258 | 259 | return 2 * (pre * rec) / (pre + rec) 260 | 261 | 262 | class Evaluator(keras.callbacks.Callback): 263 | """评估与保存 264 | """ 265 | 266 | def __init__(self): 267 | self.best_val_f1 = 0. 268 | 269 | def on_epoch_end(self, epoch, logs=None): 270 | if ada_embedding.lr_multiplier != 1: 271 | ada_embedding.lr_multiplier = ada_embedding.lr_multiplier * 0.9 272 | 273 | val_f1 = evaluate(valid_generator) 274 | if val_f1 > self.best_val_f1: 275 | self.best_val_f1 = val_f1 276 | model.save_weights('best_parimatch_model.weights') 277 | # test_acc = evaluate(test_generator) 278 | print( 279 | u'val_f1: %.5f, best_val_f1: %.5f\n' % 280 | (val_f1, self.best_val_f1) 281 | ) 282 | 283 | 284 | def predict_to_file(path='pair_submission.tsv', data=test_generator): 285 | preds = [] 286 | for x, _ in tqdm(test_generator): 287 | pred = model.predict(x)[:, 0] 288 | pred = np.round(pred) 289 | pred = pred.astype(int) 290 | preds.extend(pred) 291 | 292 | ret = [] 293 | for d, p in zip(test_data, preds): 294 | q_id, _, r_id, _, _ = d 295 | ret.append([str(q_id), str(r_id), str(p)]) 296 | 297 | with open(path, 'w', encoding='utf8') as f: 298 | for l in ret: 299 | f.write('\t'.join(l) + '\n') 300 | 301 | 302 | if __name__ == '__main__': 303 | evaluator = Evaluator() 304 | model.fit_generator( 305 | train_generator.generator(), 306 | steps_per_epoch=len(train_generator), 307 | epochs=epochs, 308 | callbacks=[evaluator], 309 | # class_weight={0:1, 1:4} 310 | ) 311 | # load best model and predict result 312 | model.load_weights('best_parimatch_model.weights') 313 | predict_to_file() 314 | -------------------------------------------------------------------------------- /pair-post-training-wwm-sop.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Date : 2020/12/1 3 | # @Author : mingming.xu 4 | # @Email : xv44586@gmail.com 5 | # @File : post-training-wwm-sop.py 6 | """ 7 | 训练样本格式为[query, reply],并替换NSP 为SOP 8 | """ 9 | import os 10 | 11 | os.environ['TF_KERAS'] = '1' # 必须使用tf.keras 12 | 13 | import numpy as np 14 | from tqdm import tqdm 15 | import jieba 16 | import itertools 17 | 18 | from toolkit4nlp.utils import DataGenerator, pad_sequences 19 | from toolkit4nlp.models import * 20 | from toolkit4nlp.tokenizers import * 21 | from toolkit4nlp.backend import * 22 | from toolkit4nlp.layers import * 23 | from toolkit4nlp.optimizers import * 24 | 25 | # config 26 | path = '/home/mingming.xu/datasets/NLP/ccf_qa_match/' 27 | 28 | p = os.path.join(path, 'train', 'train.query.tsv') 29 | config_path = '/home/mingming.xu/pretrain/NLP/nezha_base_wwm/bert_config.json' 30 | checkpoint_path = '/home/mingming.xu/pretrain/NLP/nezha_base_wwm/model.ckpt' 31 | dict_path = '/home/mingming.xu/pretrain/NLP/nezha_base_wwm/vocab.txt' 32 | model_saved_path = './nezha_post_training/wwm-model-add-dict-no-mask-end-sop.ckpt' 33 | 34 | new_dict_path = './data/new_dict.txt' 35 | maxlen = 256 36 | batch_size = 16 37 | epochs = 100 38 | learning_rate = 5e-5 39 | # 建立分词器 40 | tokenizer = Tokenizer(dict_path, do_lower_case=True) 41 | 42 | 43 | def load_data(train_test='train'): 44 | D = {} 45 | with open(os.path.join(path, train_test, train_test + '.query.tsv')) as f: 46 | for l in f: 47 | span = l.strip().split('\t') 48 | q = span[1] 49 | # if not tokenizer._is_punctuation(list(q)[-1]): 50 | # q += '。' 51 | D[span[0]] = {'query': q, 'reply': []} 52 | 53 | with open(os.path.join(path, train_test, train_test + '.reply.tsv')) as f: 54 | for l in f: 55 | span = l.strip().split('\t') 56 | if len(span) == 4: 57 | q_id, r_id, r, label = span 58 | else: 59 | label = None 60 | q_id, r_id, r = span 61 | 62 | D[q_id]['reply'].append(r) 63 | 64 | d = [] 65 | for k, v in D.items(): 66 | item = [] 67 | l = 0 68 | q = v['query'] 69 | replys = v['reply'] 70 | l += len(q) 71 | item.append(q) 72 | for r in replys: 73 | lr = len(r) 74 | 75 | l += lr 76 | item.append(r) 77 | 78 | d.append(item) 79 | 80 | return d 81 | 82 | 83 | train_data = load_data('train') 84 | test_data = load_data('test') 85 | data = train_data + test_data 86 | 87 | jieba.initialize() 88 | 89 | new_words = [] 90 | with open(new_dict_path) as f: 91 | for l in f: 92 | w = l.strip() 93 | new_words.append(w) 94 | jieba.add_word(w) 95 | 96 | words_data = [[jieba.lcut(line) for line in sen] for sen in data] 97 | 98 | pair_data = [] 99 | for line in words_data: 100 | q, rs = line[0], line[1:] 101 | for r in rs: 102 | pair_data.append([q, r]) 103 | data = pair_data 104 | np.random.shuffle(data) 105 | more_ids = batch_size - (len(data) % batch_size) 106 | data = data + data[: more_ids] 107 | 108 | 109 | def can_mask(token_ids): 110 | if token_ids in (tokenizer._token_start_id, tokenizer._token_mask_id, tokenizer._token_end_id): 111 | return False 112 | 113 | return True 114 | 115 | 116 | def random_masking(lines): 117 | """对输入进行随机mask 118 | """ 119 | 120 | # rands = np.random.random(len(token_ids)) 121 | sources, targets = [tokenizer._token_start_id], [0] 122 | segments = [0] 123 | 124 | for i, sent in enumerate(lines): 125 | source, target = [], [] 126 | segment = [] 127 | rands = np.random.random(len(sent)) 128 | for r, word in zip(rands, sent): 129 | word_token = tokenizer.encode(word)[0][1:-1] 130 | 131 | if r < 0.15 * 0.8: 132 | source.extend(len(word_token) * [tokenizer._token_mask_id]) 133 | target.extend(word_token) 134 | elif r < 0.15 * 0.9: 135 | source.extend(word_token) 136 | target.extend(word_token) 137 | elif r < 0.15: 138 | source.extend([np.random.choice(tokenizer._vocab_size - 5) + 5 for _ in range(len(word_token))]) 139 | target.extend(word_token) 140 | else: 141 | source.extend(word_token) 142 | target.extend([0] * len(word_token)) 143 | 144 | # add end token 145 | source.append(tokenizer._token_end_id) 146 | # target.append(tokenizer._token_end_id) 147 | target.append(0) 148 | 149 | if i == 0: 150 | segment = [0] * len(source) 151 | else: 152 | segment = [1] * len(source) 153 | 154 | sources.extend(source) 155 | targets.extend(target) 156 | segments.extend(segment) 157 | 158 | return sources, targets, segments 159 | 160 | 161 | class data_generator(DataGenerator): 162 | def __iter__(self, shuffle=False): 163 | batch_token_ids, batch_segment_ids, batch_target_ids, batch_is_masked, batch_nsp = [], [], [], [], [] 164 | 165 | for is_end, item in self.get_sample(shuffle): 166 | # 50% shuffle order 167 | label = 1 168 | p = np.random.random() 169 | if p < 0.5: 170 | label = 0 171 | item = item[::-1] 172 | 173 | source_tokens, target_tokens, segment_ids = random_masking(item) 174 | 175 | is_masked = [0 if i == 0 else 1 for i in target_tokens] 176 | batch_token_ids.append(source_tokens) 177 | batch_segment_ids.append(segment_ids) 178 | batch_target_ids.append(target_tokens) 179 | batch_is_masked.append(is_masked) 180 | batch_nsp.append([label]) 181 | 182 | if is_end or len(batch_token_ids) == self.batch_size: 183 | batch_token_ids = pad_sequences(batch_token_ids, maxlen=maxlen) 184 | batch_segment_ids = pad_sequences(batch_segment_ids, maxlen=maxlen) 185 | batch_target_ids = pad_sequences(batch_target_ids, maxlen=maxlen) 186 | batch_is_masked = pad_sequences(batch_is_masked, maxlen=maxlen) 187 | batch_nsp = pad_sequences(batch_nsp) 188 | 189 | yield [batch_token_ids, batch_segment_ids, batch_target_ids, batch_is_masked, batch_nsp], None 190 | 191 | batch_token_ids, batch_segment_ids, batch_target_ids, batch_is_masked = [], [], [], [] 192 | batch_nsp = [] 193 | 194 | 195 | train_generator = data_generator(data, batch_size) 196 | 197 | 198 | def build_transformer_model_with_mlm(): 199 | """带mlm的bert模型 200 | """ 201 | bert = build_transformer_model( 202 | config_path, 203 | with_mlm='linear', 204 | with_nsp=True, 205 | model='nezha', 206 | return_keras_model=False, 207 | ) 208 | proba = bert.model.output 209 | # print(proba) 210 | # 辅助输入 211 | token_ids = Input(shape=(None,), dtype='int64', name='token_ids') # 目标id 212 | is_masked = Input(shape=(None,), dtype=K.floatx(), name='is_masked') # mask标记 213 | nsp_label = Input(shape=(None,), dtype='int64', name='nsp') # nsp 214 | 215 | def mlm_loss(inputs): 216 | """计算loss的函数,需要封装为一个层 217 | """ 218 | y_true, y_pred, mask = inputs 219 | _, y_pred = y_pred 220 | loss = K.sparse_categorical_crossentropy( 221 | y_true, y_pred, from_logits=True 222 | ) 223 | loss = K.sum(loss * mask) / (K.sum(mask) + K.epsilon()) 224 | return loss 225 | 226 | def nsp_loss(inputs): 227 | """计算nsp loss的函数,需要封装为一个层 228 | """ 229 | y_true, y_pred = inputs 230 | y_pred, _ = y_pred 231 | loss = K.sparse_categorical_crossentropy( 232 | y_true, y_pred 233 | ) 234 | loss = K.mean(loss) 235 | return loss 236 | 237 | def mlm_acc(inputs): 238 | """计算准确率的函数,需要封装为一个层 239 | """ 240 | y_true, y_pred, mask = inputs 241 | _, y_pred = y_pred 242 | y_true = K.cast(y_true, K.floatx()) 243 | acc = keras.metrics.sparse_categorical_accuracy(y_true, y_pred) 244 | acc = K.sum(acc * mask) / (K.sum(mask) + K.epsilon()) 245 | return acc 246 | 247 | def nsp_acc(inputs): 248 | """计算准确率的函数,需要封装为一个层 249 | """ 250 | y_true, y_pred = inputs 251 | y_pred, _ = y_pred 252 | y_true = K.cast(y_true, K.floatx()) 253 | acc = keras.metrics.sparse_categorical_accuracy(y_true, y_pred) 254 | acc = K.mean(acc) 255 | return acc 256 | 257 | mlm_loss = Lambda(mlm_loss, name='mlm_loss')([token_ids, proba, is_masked]) 258 | mlm_acc = Lambda(mlm_acc, name='mlm_acc')([token_ids, proba, is_masked]) 259 | nsp_loss = Lambda(nsp_loss, name='nsp_loss')([nsp_label, proba]) 260 | nsp_acc = Lambda(nsp_acc, name='nsp_acc')([nsp_label, proba]) 261 | 262 | train_model = Model( 263 | bert.model.inputs + [token_ids, is_masked, nsp_label], [mlm_loss, mlm_acc, nsp_loss, nsp_acc] 264 | ) 265 | 266 | loss = { 267 | 'mlm_loss': lambda y_true, y_pred: y_pred, 268 | 'mlm_acc': lambda y_true, y_pred: K.stop_gradient(y_pred), 269 | 'nsp_loss': lambda y_true, y_pred: y_pred, 270 | 'nsp_acc': lambda y_true, y_pred: K.stop_gradient(y_pred), 271 | } 272 | 273 | return bert, train_model, loss 274 | 275 | 276 | bert, train_model, loss = build_transformer_model_with_mlm() 277 | 278 | Opt = extend_with_weight_decay(Adam) 279 | Opt = extend_with_gradient_accumulation(Opt) 280 | Opt = extend_with_piecewise_linear_lr(Opt) 281 | 282 | opt = Opt(learning_rate=learning_rate, 283 | exclude_from_weight_decay=['Norm', 'bias'], 284 | lr_schedule={int(len(train_generator) * epochs * 0.1): 1.0, len(train_generator) * epochs: 0}, 285 | weight_decay_rate=0.01, 286 | grad_accum_steps=2, 287 | ) 288 | 289 | train_model.compile(loss=loss, optimizer=opt) 290 | # 如果传入权重,则加载。注:须在此处加载,才保证不报错。 291 | if checkpoint_path is not None: 292 | bert.load_weights_from_checkpoint(checkpoint_path) 293 | 294 | train_model.summary() 295 | 296 | 297 | class ModelCheckpoint(keras.callbacks.Callback): 298 | """ 299 | 每10个epoch保存一次模型 300 | """ 301 | 302 | def __init__(self): 303 | self.loss = 1e6 304 | 305 | def on_epoch_end(self, epoch, logs=None): 306 | if logs['loss'] < self.loss: 307 | self.loss = logs['loss'] 308 | 309 | # print('epoch: {}, loss is : {}, lowest loss is:'.format(epoch, logs['loss'], self.loss)) 310 | 311 | if (epoch + 1) % 10 == 0: 312 | bert.save_weights_as_checkpoint(model_saved_path + '-{}'.format(epoch + 1)) 313 | 314 | token_ids, segment_ids = tokenizer.encode(u'看哪个?', '微信您通过一下吧') 315 | token_ids[9] = token_ids[10] = tokenizer._token_mask_id 316 | 317 | probs = bert.model.predict([np.array([token_ids]), np.array([segment_ids])])[1] 318 | print(tokenizer.decode(probs[0, 9:11].argmax(axis=1))) 319 | 320 | 321 | if __name__ == '__main__': 322 | # 保存模型 323 | checkpoint = ModelCheckpoint() 324 | # 记录日志 325 | csv_logger = keras.callbacks.CSVLogger('training.log') 326 | 327 | train_model.fit( 328 | train_generator.generator(), 329 | steps_per_epoch=len(train_generator), 330 | epochs=epochs, 331 | callbacks=[checkpoint, csv_logger], 332 | ) 333 | -------------------------------------------------------------------------------- /pair-self-kd.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Date : 2020/12/14 3 | # @Author : mingming.xu 4 | # @Email : xv44586@gmail.com 5 | # @File : pair-self-kd.py 6 | import os 7 | import numpy as np 8 | import tensorflow as tf 9 | import random 10 | import numpy as np 11 | from tqdm import tqdm 12 | 13 | from toolkit4nlp.utils import * 14 | from toolkit4nlp.models import * 15 | from toolkit4nlp.tokenizers import * 16 | from toolkit4nlp.backend import * 17 | from toolkit4nlp.layers import * 18 | from toolkit4nlp.optimizers import * 19 | 20 | seed = 0 21 | # tf.random.set_seed(seed) 22 | np.random.seed(seed) 23 | 24 | path = '/home/mingming.xu/datasets/NLP/ccf_qa_match/' 25 | 26 | maxlen = 128 27 | batch_size = 16 28 | epochs = 5 29 | Temperature = 4 # 平滑soften labels 分布,越大越平滑,一般取值[1, 10] 30 | 31 | # bert配置 32 | config_path = '/home/mingming.xu/pretrain/NLP/nezha_base_wwm/bert_config.json' 33 | checkpoint_path = './nezha_post_training/wwm-model-add-dict-no-mask-end-sop.ckpt-40' 34 | dict_path = '/home/mingming.xu/pretrain/NLP/nezha_base_wwm/vocab.txt' 35 | 36 | # 建立分词器 37 | tokenizer = Tokenizer(dict_path, do_lower_case=True) 38 | 39 | 40 | def load_data(train_test='train'): 41 | D = {} 42 | with open(os.path.join(path, train_test, train_test + '.query.tsv')) as f: 43 | for l in f: 44 | span = l.strip().split('\t') 45 | D[span[0]] = {'query': span[1], 'reply': []} 46 | 47 | with open(os.path.join(path, train_test, train_test + '.reply.tsv')) as f: 48 | for l in f: 49 | span = l.strip().split('\t') 50 | if len(span) == 4: 51 | q_id, r_id, r, label = span 52 | else: 53 | label = None 54 | q_id, r_id, r = span 55 | D[q_id]['reply'].append([r_id, r, label]) 56 | d = [] 57 | for k, v in D.items(): 58 | q_id = k 59 | q = v['query'] 60 | reply = v['reply'] 61 | 62 | c = [] 63 | l = [] 64 | for r in reply: 65 | r_id, rc, label = r 66 | 67 | d.append([q_id, q, r_id, rc, label]) 68 | return d 69 | 70 | 71 | train_data_all = load_data('train') 72 | test_data = load_data('test') 73 | 74 | 75 | # data generator 76 | def can_padding(token_id): 77 | if token_id in (tokenizer._token_mask_id, tokenizer._token_end_id, tokenizer._token_start_id): 78 | return False 79 | return True 80 | 81 | 82 | class data_generator(DataGenerator): 83 | def random_padding(self, token_ids): 84 | rands = np.random.random(len(token_ids)) 85 | new_tokens = [] 86 | for p, token in zip(rands, token_ids): 87 | if p < 0.1 and can_padding(token): 88 | new_tokens.append(tokenizer._token_pad_id) 89 | else: 90 | new_tokens.append(token) 91 | return new_tokens 92 | 93 | def __iter__(self, shuffle=False): 94 | batch_token_ids, batch_segment_ids, batch_labels, batch_soften = [], [], [], [] 95 | for is_end, item in self.get_sample(shuffle): 96 | soften = None 97 | if len(item) == 5: 98 | q_id, q, r_id, r, label = item 99 | else: 100 | # has soften 101 | q_id, q, r_id, r, label, soften = item 102 | 103 | label = int(label) if label is not None else None 104 | 105 | token_ids, segment_ids = tokenizer.encode(q, r, maxlen=256) 106 | if shuffle: 107 | token_ids = self.random_padding(token_ids) 108 | 109 | batch_token_ids.append(token_ids) 110 | batch_segment_ids.append(segment_ids) 111 | batch_labels.append([label]) 112 | batch_soften.append(soften) 113 | 114 | if is_end or len(batch_token_ids) == self.batch_size: 115 | batch_token_ids = pad_sequences(batch_token_ids) 116 | batch_segment_ids = pad_sequences(batch_segment_ids) 117 | batch_labels = pad_sequences(batch_labels) 118 | if soften is not None: 119 | batch_soften = pad_sequences(batch_soften) 120 | if len(item) == 5: 121 | yield [batch_token_ids, batch_segment_ids], batch_labels 122 | else: 123 | yield [batch_token_ids, batch_segment_ids], [batch_labels, batch_soften] 124 | 125 | batch_token_ids, batch_segment_ids, batch_labels, batch_soften = [], [], [], [] 126 | 127 | 128 | # shuffle 129 | # np.random.shuffle(train_data) 130 | # n = int(len(train_data) * 0.8) 131 | # train_generator = data_generator(train_data[:n], batch_size) 132 | # valid_generator = data_generator(train_data[n: ], batch_size) 133 | # test_generator = data_generator(test_data, batch_size) 134 | 135 | 136 | fold = 0 137 | train_data, valid_data = [], [] 138 | for idx in range(len(train_data_all)): 139 | if int(train_data_all[idx][0]) % 10 != fold: 140 | train_data.append(train_data_all[idx]) 141 | else: 142 | valid_data.append(train_data_all[idx]) 143 | 144 | train_generator = data_generator(train_data, batch_size=batch_size) 145 | valid_generator = data_generator(valid_data, batch_size=batch_size * 2) 146 | test_generator = data_generator(test_data, batch_size=batch_size * 2) 147 | 148 | # 构建Teacher model 149 | teacher = build_transformer_model( 150 | config_path=config_path, 151 | checkpoint_path=checkpoint_path, 152 | model='nezha', 153 | prefix='Teacher-' 154 | ) 155 | 156 | output = Lambda(lambda x: x[:, 0])(teacher.output) 157 | output = Dropout(0.01)(output) 158 | logits = Dense(2)(output) 159 | t_output = Activation(activation='softmax')(logits) 160 | teacher_model = keras.models.Model(teacher.input, t_output) 161 | teacher_logits = keras.models.Model(teacher.input, logits) 162 | teacher_model.summary() 163 | 164 | grad_accum_steps = 3 165 | opt = extend_with_weight_decay(Adam) 166 | opt = extend_with_gradient_accumulation(opt) 167 | exclude_from_weight_decay = ['Norm', 'bias'] 168 | opt = extend_with_piecewise_linear_lr(opt) 169 | para = { 170 | 'learning_rate': 1e-5, 171 | 'weight_decay_rate': 0.1, 172 | 'exclude_from_weight_decay': exclude_from_weight_decay, 173 | 'grad_accum_steps': grad_accum_steps, 174 | 'lr_schedule': {int(len(train_generator) * 0.1 * epochs / grad_accum_steps): 1, 175 | int(len(train_generator) * epochs / grad_accum_steps): 0}, 176 | } 177 | 178 | opt = opt(**para) 179 | teacher_model.compile( 180 | loss='sparse_categorical_crossentropy', 181 | # optimizer=Adam(2e-5), # 用足够小的学习率 182 | optimizer=opt, 183 | metrics=['accuracy'], 184 | ) 185 | 186 | 187 | def evaluate(data=valid_generator, model=None): 188 | P, R, TP = 0., 0., 0. 189 | for x_true, y_true in tqdm(data): 190 | y_pred = model.predict(x_true).argmax(-1) 191 | y_pred = np.round(y_pred) 192 | y_true = y_true[:, 0] 193 | 194 | R += y_pred.sum() 195 | P += y_true.sum() 196 | TP += ((y_pred + y_true) > 1).sum() 197 | 198 | print(P, R, TP) 199 | pre = TP / R 200 | rec = TP / P 201 | 202 | return 2 * (pre * rec) / (pre + rec) 203 | 204 | 205 | class Evaluator(keras.callbacks.Callback): 206 | """评估与保存 207 | """ 208 | 209 | def __init__(self, save_path, valid_model=None): 210 | self.best_val_f1 = 0. 211 | self.save_path = save_path 212 | self.valid_model = valid_model 213 | 214 | def on_epoch_end(self, epoch, logs=None): 215 | val_f1 = evaluate(valid_generator, self.valid_model) 216 | if val_f1 > self.best_val_f1: 217 | self.best_val_f1 = val_f1 218 | self.model.save_weights(self.save_path) 219 | # test_acc = evaluate(test_generator) 220 | print( 221 | u'val_f1: %.5f, best_val_f1: %.5f\n' % 222 | (val_f1, self.best_val_f1) 223 | ) 224 | 225 | 226 | # build student model 227 | student = build_transformer_model(config_path=config_path, 228 | checkpoint_path=checkpoint_path, 229 | model='nezha', 230 | prefix='Student-') 231 | 232 | x = Lambda(lambda x: x[:, 0])(student.output) 233 | x = Dropout(0.01)(x) 234 | s_logits = Dense(2)(x) 235 | s_output = Activation(activation='softmax')(s_logits) 236 | 237 | student_model = Model(student.input, s_output) 238 | 239 | s_logits_t = Lambda(lambda x: x / Temperature)(s_logits) 240 | s_soften = Activation(activation='softmax')(s_logits_t) 241 | 242 | student_train = Model(student.inputs, [s_output, s_soften]) 243 | 244 | student_train.summary() 245 | student_train.compile( 246 | loss=['sparse_categorical_crossentropy', keras.losses.kullback_leibler_divergence], 247 | optimizer=opt, 248 | loss_weights=[1, Temperature ** 2]) # 放大kld 249 | 250 | if __name__ == '__main__': 251 | 252 | teacher_save_path = 'pair-teacher.model' 253 | 254 | evaluator = Evaluator(save_path=teacher_save_path, valid_model=teacher_model) 255 | 256 | teacher_model.fit_generator( 257 | train_generator.generator(), 258 | steps_per_epoch=len(train_generator), 259 | epochs=epochs, 260 | callbacks=[evaluator], 261 | ) 262 | teacher_model.load_weights(teacher_save_path) 263 | evaluate(valid_generator, teacher_model) 264 | 265 | # create logits 266 | logits = [] 267 | for x, _ in tqdm(data_generator(train_data_all, 64)): 268 | logit_ = teacher_logits.predict(x) 269 | logits.append(logit_) 270 | 271 | logits = np.concatenate(logits, axis=0) 272 | logits.dump('train_all.logits') 273 | 274 | # logits = np.load('train_all.logits', allow_pickle=True) 275 | train_data_logits = [] 276 | for d, l in zip(train_data_all, logits): 277 | soften = K.softmax(l / Temperature).numpy().tolist() 278 | train_data_logits.append(d + [soften]) 279 | 280 | train_data = [] 281 | for idx in range(len(train_data_logits)): 282 | if int(train_data_logits[idx][0]) % 10 != fold: 283 | train_data.append(train_data_logits[idx]) 284 | 285 | student_train_generator = data_generator(train_data, batch_size=batch_size) 286 | 287 | student_save_path = 'pair-student.model' 288 | student_evaluator = Evaluator(student_save_path, student_model) 289 | 290 | student_train.fit_generator(student_train_generator.generator(), 291 | steps_per_epoch=len(student_train_generator), 292 | epochs=epochs, 293 | callbacks=[student_evaluator]) 294 | 295 | student_train.load_weights(student_save_path) 296 | evaluate(valid_generator, student_model) 297 | -------------------------------------------------------------------------------- /pair-supervised-contrastive-learning.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Date : 2020/12/4 3 | # @Author : mingming.xu 4 | # @Email : xv44586@gmail.com 5 | # @File : pair-supervised-contrastive-learning.py 6 | """ 7 | 拉近batch 内相同label 样本在特征空间的距离 8 | ref: [Supervised Contrastive Learning](https://arxiv.org/pdf/2004.11362.pdf) 9 | """ 10 | 11 | import os 12 | import numpy as np 13 | from tqdm import tqdm 14 | 15 | from toolkit4nlp.utils import * 16 | from toolkit4nlp.models import * 17 | from toolkit4nlp.tokenizers import * 18 | from toolkit4nlp.backend import * 19 | from toolkit4nlp.layers import * 20 | from toolkit4nlp.optimizers import * 21 | 22 | path = '/home/mingming.xu/datasets/NLP/ccf_qa_match/' 23 | 24 | maxlen = 128 25 | batch_size = 32 26 | epochs = 5 27 | 28 | config_path = '/home/mingming.xu/pretrain/NLP/nezha_base_wwm/bert_config.json' 29 | checkpoint_path = '/home/mingming.xu/pretrain/NLP/nezha_base_wwm/model.ckpt' 30 | dict_path = '/home/mingming.xu/pretrain/NLP/nezha_base_wwm/vocab.txt' 31 | 32 | # 建立分词器 33 | token_dict, keep_tokens = load_vocab(dict_path, 34 | simplified=True, 35 | startswith=['[PAD]', '[UNK]', '[MASK]', '[CLS]', '[SEP]']) 36 | 37 | tokenizer = Tokenizer(token_dict, do_lower_case=True) 38 | 39 | 40 | def load_data(train_test='train'): 41 | D = {} 42 | with open(os.path.join(path, train_test, train_test + '.query.tsv')) as f: 43 | for l in f: 44 | span = l.strip().split('\t') 45 | D[span[0]] = {'query': span[1], 'reply': []} 46 | 47 | with open(os.path.join(path, train_test, train_test + '.reply.tsv')) as f: 48 | for l in f: 49 | span = l.strip().split('\t') 50 | if len(span) == 4: 51 | q_id, r_id, r, label = span 52 | else: 53 | label = None 54 | q_id, r_id, r = span 55 | D[q_id]['reply'].append([r_id, r, label]) 56 | d = [] 57 | for k, v in D.items(): 58 | q_id = k 59 | q = v['query'] 60 | reply = v['reply'] 61 | 62 | c = [] 63 | l = [] 64 | for r in reply: 65 | r_id, rc, label = r 66 | 67 | d.append([q_id, q, r_id, rc, label]) 68 | return d 69 | 70 | 71 | train_data = load_data('train') 72 | test_data = load_data('test') 73 | 74 | 75 | def can_padding(token_id): 76 | if token_id in (tokenizer._token_mask_id, tokenizer._token_end_id, tokenizer._token_start_id): 77 | return False 78 | return True 79 | 80 | 81 | class data_generator(DataGenerator): 82 | def random_padding(self, token_ids): 83 | rands = np.random.random(len(token_ids)) 84 | new_tokens = [] 85 | for p, token in zip(rands, token_ids): 86 | if p < 0.1 and can_padding(token): 87 | new_tokens.append(tokenizer._token_pad_id) 88 | else: 89 | new_tokens.append(token) 90 | return new_tokens 91 | 92 | def __iter__(self, shuffle=False): 93 | batch_token_ids, batch_segment_ids, batch_labels = [], [], [] 94 | for is_end, (q_id, q, r_id, r, label) in self.get_sample(shuffle): 95 | label = float(label) if label is not None else None 96 | 97 | # if shuffle: 98 | # token_ids, segment_ids = tokenizer.encode(q, r,maxlen=maxlen ) 99 | # token_ids = self.random_padding(token_ids) 100 | 101 | # else: 102 | token_ids, segment_ids = tokenizer.encode(q, r, maxlen=maxlen) 103 | 104 | batch_token_ids.append(token_ids) 105 | batch_segment_ids.append(segment_ids) 106 | batch_labels.append([label]) 107 | 108 | if is_end or len(batch_token_ids) == self.batch_size: 109 | batch_token_ids = pad_sequences(batch_token_ids) 110 | batch_segment_ids = pad_sequences(batch_segment_ids) 111 | batch_labels = pad_sequences(batch_labels) 112 | 113 | yield [batch_token_ids, batch_segment_ids, batch_labels], None 114 | 115 | batch_token_ids, batch_segment_ids, batch_labels = [], [], [] 116 | 117 | 118 | # shuffle 119 | np.random.shuffle(train_data) 120 | n = int(len(train_data) * 0.8) 121 | valid_data, train_data = train_data[n:], train_data[:n] 122 | train_generator = data_generator(train_data, batch_size) 123 | valid_generator = data_generator(valid_data, batch_size) 124 | test_generator = data_generator(test_data, batch_size) 125 | 126 | 127 | class SupervisedContrastiveLearning(Loss): 128 | """https://arxiv.org/pdf/2011.01403.pdf""" 129 | 130 | def __init__(self, alpha=1., T=1., **kwargs): 131 | super(SupervisedContrastiveLearning, self).__init__(**kwargs) 132 | self.alpha = alpha # loss weight 133 | self.T = T # Temperature 134 | 135 | def compute_loss(self, inputs, mask=None): 136 | loss = self.compute_loss_of_scl(inputs) 137 | loss = loss * self.alpha 138 | self.add_metric(loss, name='scl_loss') 139 | return loss 140 | 141 | def get_label_mask(self, y_true): 142 | """获取batch内相同label样本""" 143 | label = K.cast(y_true, 'int32') 144 | label_2 = K.reshape(label, (1, -1)) 145 | mask = K.equal(label_2, label) 146 | mask = K.cast(mask, K.floatx()) 147 | mask = mask * (1 - K.eye(K.shape(y_true)[0])) # 排除对角线,即 i == j 148 | return mask 149 | 150 | def compute_loss_of_scl(self, inputs, mask=None): 151 | y_pred, y_true = inputs 152 | label_mask = self.get_label_mask(y_true) 153 | y_pred = K.l2_normalize(y_pred, axis=1) # 特征向量归一化 154 | similarities = K.dot(y_pred, K.transpose(y_pred)) # 相似矩阵 155 | similarities = similarities - K.eye(K.shape(y_pred)[0]) * 1e12 # 排除对角线,即 i == j 156 | 157 | similarities = similarities / self.T # Temperature scale 158 | similarities = K.exp(similarities) # exp 159 | 160 | sum_similarities = K.sum(similarities, axis=-1, keepdims=True) # sum i != k 161 | scl = similarities / sum_similarities 162 | scl = K.log((scl + K.epsilon())) # sum log 163 | scl = -K.sum(scl * label_mask, axis=1, keepdims=True) / (K.sum(label_mask, axis=1, keepdims=True) + K.epsilon()) 164 | return K.mean(scl) 165 | 166 | 167 | class CrossEntropy(Loss): 168 | def compute_loss(self, inputs, mask=None): 169 | pred, ytrue = inputs 170 | ytrue = K.cast(ytrue, K.floatx()) 171 | loss = K.binary_crossentropy(ytrue, pred) 172 | loss = K.mean(loss) 173 | self.add_metric(loss, name='clf_loss') 174 | return loss 175 | 176 | 177 | # 加载预训练模型 178 | bert = build_transformer_model( 179 | config_path=config_path, 180 | checkpoint_path=checkpoint_path, 181 | model='nezha', 182 | keep_tokens=keep_tokens, 183 | num_hidden_layers=12, 184 | ) 185 | output = Lambda(lambda x: x[:, 0])(bert.output) 186 | y_in = Input(shape=(None,)) 187 | 188 | scl_output = SupervisedContrastiveLearning(alpha=0.1, T=0.2, output_idx=0)([output, y_in]) 189 | 190 | output = Dropout(0.1)(output) 191 | 192 | clf_output = Dense(1, activation='sigmoid')(output) 193 | clf = CrossEntropy(0)([clf_output, y_in]) 194 | model = keras.models.Model(bert.input, clf_output) 195 | model.summary() 196 | 197 | train_model = keras.models.Model(bert.input + [y_in], [scl_output, clf]) 198 | 199 | optimizer = extend_with_weight_decay(Adam) 200 | optimizer = extend_with_piecewise_linear_lr(optimizer) 201 | opt = optimizer(learning_rate=1e-5, weight_decay_rate=0.1, exclude_from_weight_decay=['Norm', 'bias'], 202 | lr_schedule={int(len(train_generator) * 0.1 * epochs): 1, len(train_generator) * epochs: 0} 203 | ) 204 | 205 | train_model.compile( 206 | optimizer=opt, 207 | ) 208 | 209 | 210 | def evaluate(data): 211 | P, R, TP = 0., 0., 0. 212 | for x, _ in tqdm(data): 213 | x_true = x[:2] 214 | y_true = x[-1] 215 | y_pred = model.predict(x_true)[:, 0] 216 | y_pred = np.round(y_pred) 217 | 218 | y_true = y_true[:, 0] 219 | R += y_pred.sum() 220 | P += y_true.sum() 221 | TP += ((y_pred + y_true) > 1).sum() 222 | 223 | print(P, R, TP) 224 | pre = TP / R 225 | rec = TP / P 226 | 227 | return 2 * (pre * rec) / (pre + rec) 228 | 229 | 230 | class Evaluator(keras.callbacks.Callback): 231 | """评估与保存 232 | """ 233 | 234 | def __init__(self, save_path): 235 | self.best_val_f1 = 0. 236 | self.save_path = save_path 237 | 238 | def on_epoch_end(self, epoch, logs=None): 239 | val_f1 = evaluate(valid_generator) 240 | if val_f1 > self.best_val_f1: 241 | self.best_val_f1 = val_f1 242 | model.save_weights(self.save_path) 243 | print( 244 | u'val_f1: %.5f, best_val_f1: %.5f\n' % 245 | (val_f1, self.best_val_f1) 246 | ) 247 | 248 | 249 | def predict_to_file(path='pair_submission.tsv'): 250 | preds = [] 251 | for x, _ in tqdm(test_generator): 252 | x = x[:2] 253 | pred = model.predict(x).argmax(axis=1) 254 | # pred = np.round(pred) 255 | pred = pred.astype(int) 256 | preds.append(pred) 257 | preds = np.concatenate(preds) 258 | ret = [] 259 | for d, p in zip(test_data, preds): 260 | q_id, _, r_id, _, _ = d 261 | ret.append([str(q_id), str(r_id), str(p)]) 262 | 263 | with open(path, 'w', encoding='utf8') as f: 264 | for l in ret: 265 | f.write('\t'.join(l) + '\n') 266 | 267 | 268 | if __name__ == '__main__': 269 | save_path = 'best_pair_scl_model.weights' 270 | evaluator = Evaluator(save_path) 271 | train_model.fit_generator( 272 | train_generator.generator(), 273 | steps_per_epoch=len(train_generator), 274 | epochs=epochs, 275 | callbacks=[evaluator], 276 | ) 277 | model.load_weights(save_path) 278 | predict_to_file('pair_scl.tsv') 279 | -------------------------------------------------------------------------------- /point-post-training-wwm-sop.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Date : 2020/12/1 3 | # @Author : mingming.xu 4 | # @Email : xv44586@gmail.com 5 | # @File : post-training-wwm-sop.py 6 | """ 7 | 训练样本格式为:[query, reply1, reply2,..], 此外替换NSP 为SOP,且SOP 时只替换reply list 顺序 8 | """ 9 | import os 10 | 11 | os.environ['TF_KERAS'] = '1' # 必须使用tf.keras 12 | 13 | import numpy as np 14 | from tqdm import tqdm 15 | import jieba 16 | import itertools 17 | 18 | from toolkit4nlp.utils import DataGenerator, pad_sequences 19 | from toolkit4nlp.models import * 20 | from toolkit4nlp.tokenizers import * 21 | from toolkit4nlp.backend import * 22 | from toolkit4nlp.layers import * 23 | from toolkit4nlp.optimizers import * 24 | 25 | # config 26 | path = '/home/mingming.xu/datasets/NLP/ccf_qa_match/' 27 | 28 | p = os.path.join(path, 'train', 'train.query.tsv') 29 | config_path = '/home/mingming.xu/pretrain/NLP/nezha_base_wwm/bert_config.json' 30 | checkpoint_path = '/home/mingming.xu/pretrain/NLP/nezha_base_wwm/model.ckpt' 31 | dict_path = '/home/mingming.xu/pretrain/NLP/nezha_base_wwm/vocab.txt' 32 | model_saved_path = './nezha_post_training/wwm-model-add-dict-no-mask-end-sop.ckpt' 33 | 34 | new_dict_path = './data/new_dict.txt' 35 | maxlen = 256 36 | batch_size = 16 37 | epochs = 100 38 | learning_rate = 5e-5 39 | # 建立分词器 40 | tokenizer = Tokenizer(dict_path) 41 | 42 | 43 | # query /reply 拼接作为训练句子 44 | def load_data(train_test='train'): 45 | D = {} 46 | with open(os.path.join(path, train_test, train_test + '.query.tsv')) as f: 47 | for l in f: 48 | span = l.strip().split('\t') 49 | q = span[1] 50 | D[span[0]] = {'query': q, 'reply': []} 51 | 52 | with open(os.path.join(path, train_test, train_test + '.reply.tsv')) as f: 53 | for l in f: 54 | span = l.strip().split('\t') 55 | if len(span) == 4: 56 | q_id, r_id, r, label = span 57 | else: 58 | q_id, r_id, r = span 59 | 60 | # if len(r) < 4 or (len(r) == 1 and tokenizer._is_punctuation(r)): 61 | # continue 62 | 63 | # 补上句号 64 | # if not tokenizer._is_punctuation(list(r)[-1]): 65 | # r += '。' 66 | 67 | D[q_id]['reply'].append(r) 68 | 69 | d = [] 70 | for k, v in D.items(): 71 | item = [] 72 | l = 0 73 | q = v['query'] 74 | replys = v['reply'] 75 | l += len(q) 76 | item.append(q) 77 | for r in replys: 78 | lr = len(r) 79 | # if l + lr >maxlen: 80 | # d.append(item) 81 | # item = [] 82 | # l = 0 83 | 84 | l += lr 85 | item.append(r) 86 | 87 | d.append(item) 88 | 89 | return d 90 | 91 | 92 | train_data = load_data('train') 93 | test_data = load_data('test') 94 | data = train_data + test_data 95 | 96 | # wwm 97 | jieba.initialize() 98 | 99 | new_words = [] 100 | with open(new_dict_path) as f: 101 | for l in f: 102 | w = l.strip() 103 | new_words.append(w) 104 | jieba.add_word(w) 105 | 106 | words_data = [[jieba.lcut(line) for line in sen] for sen in data] 107 | 108 | 109 | def shuffle_reply(item): 110 | """ 111 | 只打乱reply list的顺序 112 | """ 113 | q, rs = item[0], item[1:] 114 | permuter_rs = list(itertools.permutations(rs))[1:] 115 | if len(permuter_rs) < 1: 116 | print(item) 117 | idx = np.random.choice(len(permuter_rs)) 118 | r = permuter_rs[idx] 119 | return [q] + list(r) 120 | 121 | 122 | def can_mask(token_ids): 123 | if token_ids in (tokenizer._token_start_id, tokenizer._token_mask_id, tokenizer._token_end_id): 124 | return False 125 | 126 | return True 127 | 128 | 129 | def random_masking(lines): 130 | """对输入进行随机mask 131 | """ 132 | 133 | # rands = np.random.random(len(token_ids)) 134 | sources, targets = [tokenizer._token_start_id], [0] 135 | segments = [0] 136 | 137 | for i, sent in enumerate(lines): 138 | source, target = [], [] 139 | segment = [] 140 | rands = np.random.random(len(sent)) 141 | for r, word in zip(rands, sent): 142 | word_token = tokenizer.encode(word)[0][1:-1] 143 | 144 | if r < 0.15 * 0.8: 145 | source.extend(len(word_token) * [tokenizer._token_mask_id]) 146 | target.extend(word_token) 147 | elif r < 0.15 * 0.9: 148 | source.extend(word_token) 149 | target.extend(word_token) 150 | elif r < 0.15: 151 | source.extend([np.random.choice(tokenizer._vocab_size - 5) + 5 for _ in range(len(word_token))]) 152 | target.extend(word_token) 153 | else: 154 | source.extend(word_token) 155 | target.extend([0] * len(word_token)) 156 | 157 | # add end token 158 | source.append(tokenizer._token_end_id) 159 | # target.append(tokenizer._token_end_id) # if mask end token, use this line 160 | target.append(0) 161 | 162 | if i == 0: 163 | segment = [0] * len(source) 164 | else: 165 | segment = [1] * len(source) 166 | 167 | sources.extend(source) 168 | targets.extend(target) 169 | segments.extend(segment) 170 | 171 | return sources, targets, segments 172 | 173 | 174 | class data_generator(DataGenerator): 175 | def __iter__(self, shuffle=False): 176 | batch_token_ids, batch_segment_ids, batch_target_ids, batch_is_masked, batch_nsp = [], [], [], [], [] 177 | 178 | for is_end, item in self.get_sample(shuffle): 179 | # 50% shuffle order 180 | label = 1 181 | p = np.random.random() 182 | if p < 0.5: 183 | label = 0 184 | item = shuffle_reply(item) 185 | 186 | source_tokens, target_tokens, segment_ids = random_masking(item) 187 | 188 | is_masked = [0 if i == 0 else 1 for i in target_tokens] 189 | batch_token_ids.append(source_tokens) 190 | batch_segment_ids.append(segment_ids) 191 | batch_target_ids.append(target_tokens) 192 | batch_is_masked.append(is_masked) 193 | batch_nsp.append([label]) 194 | 195 | if is_end or len(batch_token_ids) == self.batch_size: 196 | batch_token_ids = pad_sequences(batch_token_ids, maxlen=maxlen) 197 | batch_segment_ids = pad_sequences(batch_segment_ids, maxlen=maxlen) 198 | batch_target_ids = pad_sequences(batch_target_ids, maxlen=maxlen) 199 | batch_is_masked = pad_sequences(batch_is_masked, maxlen=maxlen) 200 | batch_nsp = pad_sequences(batch_nsp) 201 | 202 | yield [batch_token_ids, batch_segment_ids, batch_target_ids, batch_is_masked, batch_nsp], None 203 | 204 | batch_token_ids, batch_segment_ids, batch_target_ids, batch_is_masked = [], [], [], [] 205 | batch_nsp = [] 206 | 207 | 208 | train_generator = data_generator(words_data, batch_size) 209 | 210 | 211 | def build_transformer_model_with_mlm(): 212 | """带mlm的bert模型 213 | """ 214 | bert = build_transformer_model( 215 | config_path, 216 | with_mlm='linear', 217 | with_nsp=True, 218 | model='nezha', 219 | return_keras_model=False, 220 | ) 221 | proba = bert.model.output 222 | # print(proba) 223 | # 辅助输入 224 | token_ids = Input(shape=(None,), dtype='int64', name='token_ids') # 目标id 225 | is_masked = Input(shape=(None,), dtype=K.floatx(), name='is_masked') # mask标记 226 | nsp_label = Input(shape=(None,), dtype='int64', name='nsp') # nsp 227 | 228 | def mlm_loss(inputs): 229 | """计算loss的函数,需要封装为一个层 230 | """ 231 | y_true, y_pred, mask = inputs 232 | _, y_pred = y_pred 233 | loss = K.sparse_categorical_crossentropy( 234 | y_true, y_pred, from_logits=True 235 | ) 236 | loss = K.sum(loss * mask) / (K.sum(mask) + K.epsilon()) 237 | return loss 238 | 239 | def nsp_loss(inputs): 240 | """计算nsp loss的函数,需要封装为一个层 241 | """ 242 | y_true, y_pred = inputs 243 | y_pred, _ = y_pred 244 | loss = K.sparse_categorical_crossentropy( 245 | y_true, y_pred 246 | ) 247 | loss = K.mean(loss) 248 | return loss 249 | 250 | def mlm_acc(inputs): 251 | """计算准确率的函数,需要封装为一个层 252 | """ 253 | y_true, y_pred, mask = inputs 254 | _, y_pred = y_pred 255 | y_true = K.cast(y_true, K.floatx()) 256 | acc = keras.metrics.sparse_categorical_accuracy(y_true, y_pred) 257 | acc = K.sum(acc * mask) / (K.sum(mask) + K.epsilon()) 258 | return acc 259 | 260 | def nsp_acc(inputs): 261 | """计算准确率的函数,需要封装为一个层 262 | """ 263 | y_true, y_pred = inputs 264 | y_pred, _ = y_pred 265 | y_true = K.cast(y_true, K.floatx()) 266 | acc = keras.metrics.sparse_categorical_accuracy(y_true, y_pred) 267 | acc = K.mean(acc) 268 | return acc 269 | 270 | mlm_loss = Lambda(mlm_loss, name='mlm_loss')([token_ids, proba, is_masked]) 271 | mlm_acc = Lambda(mlm_acc, name='mlm_acc')([token_ids, proba, is_masked]) 272 | nsp_loss = Lambda(nsp_loss, name='nsp_loss')([nsp_label, proba]) 273 | nsp_acc = Lambda(nsp_acc, name='nsp_acc')([nsp_label, proba]) 274 | 275 | train_model = Model( 276 | bert.model.inputs + [token_ids, is_masked, nsp_label], [mlm_loss, mlm_acc, nsp_loss, nsp_acc] 277 | ) 278 | 279 | loss = { 280 | 'mlm_loss': lambda y_true, y_pred: y_pred, 281 | 'mlm_acc': lambda y_true, y_pred: K.stop_gradient(y_pred), 282 | 'nsp_loss': lambda y_true, y_pred: y_pred, 283 | 'nsp_acc': lambda y_true, y_pred: K.stop_gradient(y_pred), 284 | } 285 | 286 | return bert, train_model, loss 287 | 288 | 289 | bert, train_model, loss = build_transformer_model_with_mlm() 290 | 291 | Opt = extend_with_weight_decay(Adam) 292 | Opt = extend_with_gradient_accumulation(Opt) 293 | Opt = extend_with_piecewise_linear_lr(Opt) 294 | 295 | opt = Opt(learning_rate=learning_rate, 296 | exclude_from_weight_decay=['Norm', 'bias'], 297 | lr_schedule={int(len(train_generator) * epochs * 0.1): 1.0, len(train_generator) * epochs: 0}, 298 | weight_decay_rate=0.01, 299 | grad_accum_steps=2, 300 | ) 301 | 302 | train_model.compile(loss=loss, optimizer=opt) 303 | # 如果传入权重,则加载。注:须在此处加载,才保证不报错。 304 | if checkpoint_path is not None: 305 | bert.load_weights_from_checkpoint(checkpoint_path) 306 | 307 | train_model.summary() 308 | 309 | 310 | class ModelCheckpoint(keras.callbacks.Callback): 311 | """ 312 | 每10个epoch保存一次模型 313 | """ 314 | 315 | def __init__(self): 316 | self.loss = 1e6 317 | 318 | def on_epoch_end(self, epoch, logs=None): 319 | if logs['loss'] < self.loss: 320 | self.loss = logs['loss'] 321 | 322 | # print('epoch: {}, loss is : {}, lowest loss is:'.format(epoch, logs['loss'], self.loss)) 323 | 324 | if (epoch + 1) % 10 == 0: 325 | bert.save_weights_as_checkpoint(model_saved_path + '-{}'.format(epoch + 1)) 326 | 327 | token_ids, segment_ids = tokenizer.encode(u'看哪个?', '微信您通过一下吧') 328 | token_ids[9] = token_ids[10] = tokenizer._token_mask_id 329 | 330 | probs = bert.model.predict([np.array([token_ids]), np.array([segment_ids])])[1] 331 | print(tokenizer.decode(probs[0, 9:11].argmax(axis=1))) 332 | 333 | 334 | if __name__ == '__main__': 335 | # 保存模型 336 | checkpoint = ModelCheckpoint() 337 | # 记录日志 338 | csv_logger = keras.callbacks.CSVLogger('training.log') 339 | 340 | train_model.fit( 341 | train_generator.generator(), 342 | steps_per_epoch=len(train_generator), 343 | epochs=epochs, 344 | callbacks=[checkpoint, csv_logger], 345 | ) 346 | -------------------------------------------------------------------------------- /requirements-post-training.txt: -------------------------------------------------------------------------------- 1 | jieba==0.42.1 2 | Keras==2.3.0 3 | tensorboard==1.14.0 4 | tensorflow-gpu==1.14.0 5 | tensorflow-estimator==1.14.0 6 | toolkit4nlp==0.5.0 7 | tqdm==4.51.0 8 | nlp-zero==0.1.6 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | toolkit4nlp>=0.5.0 2 | #tensorflow==2.2.0 3 | tensorflow-gpu==2.2.0 4 | keras==2.3.0 5 | nlp-zero==0.1.6 --------------------------------------------------------------------------------