摘要:
合集:自然语言处理-大语言模型微调
AI问题:模型微调、文本中的实体识别
模型:Qwen2.5-1.5B-Instruct模型
数据集:Chinese NER SFT
数据集价值:用于训练或优化模型在特定领域(如医疗、金融、司法等)的实体识别能力。
解决方案:低秩适配/LoRA微调
一、问题描述
1、模型微调
在人工智能领域,像Qwen、GPT-3、BERT这样的预训练大模型,就像读遍互联网文本的”超级学霸”。它们掌握了语言的基本规律、常识性知识和基础推理能力。但当我们需要它们处理特定任务时——比如医疗诊断、法律文书生成或客服对话——这些”通才”模型往往会显得”水土不服”。微调的目的包括:
- 让模型生成符合特定风格的文本(如学术论文或广告文案)。
- 用领域数据(如医学文献、财报)微调,增强模型的专业术语理解和生成能力。
- 通过指令微调(Instruction Tuning)或人类反馈强化学习(RLHF),使模型:遵循指令更精准(如按步骤回答问题)。符合安全、伦理规范(如拒绝不当请求)。
2、文本中的实体识别问题
文本中的实体识别问题(通常称为命名实体识别,Named Entity Recognition, NER)是指从非结构化文本中自动识别、定位并分类特定类型实体的任务。这些实体通常包括人名、地名、组织机构名、时间、日期、货币、百分比等具有明确语义意义的词汇或短语。识别文本中的实体任务还包括从大量文本中进行信息检索与抽取。识别文本中的实体属于语言理解类任务,是自然语言处理(NLP)中的一项核心任务。
核心目标:
- 识别实体:从文本中找到具有特定意义的词汇。例如:在句子“巴黎是法国的首都”中,识别出“巴黎”(地点)、“法国”(国家)。
- 分类实体类型:为每个识别出的实体标注其所属的类别。例如:“苹果”可能是水果(如果上下文是饮食)或公司(如果上下文是科技新闻)。
主要分类:
- 信息提取:从新闻、论文、报告中提取关键信息(如人物、地点、事件)。示例:从“马斯克宣布特斯拉将在柏林建厂”中提取“马斯克”(人名)、“特斯拉”(公司)、“柏林”(地点)。
- 信息检索(IR):从大规模文本中检索相关文档(如搜索引擎)。
- 问答系统(QA):回答基于实体的问题,如“谁发明了电话?” → 提取“贝尔”(人名)。
- 知识图谱构建:将文本中的实体及其关系转化为结构化知识(如“姚明 → 篮球运动员 → 中国”)。
- 特定实体的舆情分析:监控特定实体(如品牌、人物)在社交媒体中的提及情况。
3、模型选择
以Qwen2系列的模型为例:
- Qwen2-1.5b:15亿参数适合中小型企业本地化部署。纯Decoder结构,采用Grouped-Query Attention (GQA) 加速推理。
- Qwen2-VL-2b:轻量化设计:20亿参数在边缘设备(如手机)上可高效部署。支持图像识别、图文问答、图表解析等任务(如上传图片问“这是什么植物?”)。基于ViT+Transformer的视觉-语言对齐架构,动态分辨率图像输入。
- GLM4-9b:90亿参数在复杂逻辑推理、长文本理解(10万+ tokens)上表现突出。
本文选择 Qwen2.5-1.5B-Instruct模型为例,讨论在文本中的实体识别/NER任务上进行微调。
二、数据集内容
1、命名实体识别数据集样例
Chinese NER SFT 是专为中文命名实体识别(NER)任务设计的监督微调数据集,用于训练或优化模型在特定领域(如医疗、金融、司法等)的实体识别能力。其核心特点是:
- 领域适配性:包含通用语料和垂直领域标注数据。
- 细粒度标注:部分数据集支持嵌套实体、长实体识别(如“上海市浦东新区”作为整体
LOC而非分开)。 - 多格式支持:提供BIO/BIOES、JSON、CoNLL等常见标注格式。
文件名:ccfbdci.jsonl
2、数据样例
数据源文件 ccfbdci.jsonl 数据样例:
{
"text": "分析师指出,国安基金进场护盘,在护盘中心金融股以及传统产业股的带动之下,大盘呈现了破地拉升、开低走高的格局。",
"entities": [{
"start_idx": 6,
"end_idx": 10,
"entity_text": "国安基金",
"entity_label": "ORG",
"entity_names": ["组织", "团体", "机构"]
}],
"data_source": "CCFBDCI"
}
{
"text": "而连日疲软的电子股也出现了长期投资买盘,空头指标、半导体股跌停打开。",
"entities": [],
"data_source": "CCFBDCI"
}
{
"text": "菲律宾总统埃斯特拉达2号透过马尼拉当地电台宣布说,在仍遭到激进的回教阿卜沙耶夫组织羁押在非国南部和落岛的16名人质当中,军方已经营救出了11名菲律宾人质。",
"entities": [{
"start_idx": 0,
"end_idx": 3,
"entity_text": "菲律宾",
"entity_label": "GPE",
"entity_names": ["地缘政治实体", "政治实体", "地理实体", "社会实体"]
}, {
"start_idx": 5,
"end_idx": 10,
"entity_text": "埃斯特拉达",
"entity_label": "PER",
"entity_names": ["人名", "姓名"]
}, {
"start_idx": 14,
"end_idx": 17,
"entity_text": "马尼拉",
"entity_label": "GPE",
"entity_names": ["地缘政治实体", "政治实体", "地理实体", "社会实体"]
}, {
"start_idx": 34,
"end_idx": 39,
"entity_text": "阿卜沙耶夫",
"entity_label": "PER",
"entity_names": ["人名", "姓名"]
}, {
"start_idx": 44,
"end_idx": 46,
"entity_text": "非国",
"entity_label": "GPE",
"entity_names": ["地缘政治实体", "政治实体", "地理实体", "社会实体"]
}, {
"start_idx": 48,
"end_idx": 51,
"entity_text": "和落岛",
"entity_label": "LOC",
"entity_names": ["地址", "地点", "地名"]
}, {
"start_idx": 71,
"end_idx": 74,
"entity_text": "菲律宾",
"entity_label": "GPE",
"entity_names": ["地缘政治实体", "政治实体", "地理实体", "社会实体"]
}],
"data_source": "CCFBDCI"
}
三、解决方案样例
1、LoRA微调解决方案
LoRA(Low-Rank Adaptation,低秩适配)微调的核心思想是将预训练模型的权重矩阵分解为两个低秩矩阵的乘积。假设原有矩阵权重为W微调过程中拆解成两个低秩矩阵A、B的乘积,冻结原有矩阵+两个低秩矩阵的乘积来适应新的任务。对于LoRA解决方案,主模型的核心结构不变,仍然使用原始训练好的参数。LORA 模块则在不干扰主干的前提下,对最终输出产生影响。整体就像给模型“加了一个外接调节器”,使其在推理时输出更加贴合任务需求。目前主流的微调方式就是LoRA低秩矩阵微调。其优点包括:
- 减少了训练参数数量,从而降低了计算和存储成本。资源少的情况下依然可以进行模型微调。
- 性能接近全量参数微调。通过插入的低秩矩阵,LORA 能有效捕捉任务相关特征。在大多数 NLP 和生成任务中,其性能与全量微调相近。
- 模块小巧,部署灵活,即插即用。LORA 微调产生的模块通常仅几十到几百 MB,便于存储、分享与快速切换。它与原始大模型解耦,推理时加载目标 LORA 模块即可,无需改动底座模型,真正实现“即插即用”。
2、安装开发包
torch
transformers
datasets
peft
accelerate
pandas
tiktoken
modelscope
其中 peft(Parameter-Efficient Fine-Tuning,参数高效微调)是一个用于大模型轻量化微调的 Python 库,由 Hugging Face 团队开发。它的核心目标是大幅减少微调大型语言模型(LLM)时的计算资源和显存占用,同时保持模型性能。
四、微调训练过程
源码:train_qwen2.5_ner.py
使用通义千问Qwen2.5-1.5B-Instruct模型在命名实体识别(NER)任务上的微调过程。
输入模型: Qwen/Qwen2.5-1.5B-Instruct
输出LoRA模块/低秩矩阵: output_dir=”./output/Qwen2-NER”
工作流程
- 数据准备:转换原始NER数据为指令格式
- 模型加载:下载并初始化Qwen2-1.5B-Instruct模型
- LoRA配置:设置低秩适应参数
- 训练配置:定义训练参数
- 训练执行:在预处理的数据集上微调模型
- 测试评估:在测试集上验证模型性能并记录结果
这种方法的优势在于利用大语言模型的强大理解能力,通过指令微调使其适应特定领域的NER任务,同时通过LoRA技术实现了高效的参数微调。
1、引入开发包
import json
import pandas as pd
import torch
from datasets import Dataset
from modelscope import snapshot_download, AutoTokenizer
from peft import LoraConfig, TaskType, get_peft_model
from peft import PeftModel
from transformers import AutoModelForCausalLM, TrainingArguments, Trainer, DataCollatorForSeq2Seq
from transformers import TrainerCallback
import numpy as np
import os
PeftModel 是 Hugging Face PEFT(Parameter-Efficient Fine-Tuning) 库中的核心类,用于对预训练模型进行参数高效微调。它通过封装原始模型并注入轻量级适配模块(如 LoRA、Adapter 等),实现在极少量参数可训练的情况下高效适配下游任务。
2、数据预处理
将原始NER数据集转换为适合大模型微调的指令格式。将数据集处理成模型可以接受的输入格式。
处理过程:
- 构建系统提示(system prompt)和用户输入。
- 使用tokenizer将文本转换为token IDs。
- 处理输入和输出的attention mask和labels。
- 对过长的序列进行截断(MAX_LENGTH=384)。
3、模型加载与配置
模型下载与加载
- 下载Qwen2-1.5B-Instruct模型
- 加载tokenizer和模型:
- tokenizer配置为不使用fast版本,信任远程代码
- 模型使用自动设备映射,bfloat16精度
model_name = "Qwen/Qwen2.5-1.5B-Instruct"
# Transformers加载模型权重
tokenizer = AutoTokenizer.from_pretrained(model_name, use_fast=False, trust_remote_code=True)
# 显式初始化 meta tensor(显存不足时)
model = AutoModelForCausalLM.from_pretrained(
model_name,
device_map="auto",
offload_folder="./offload", # 指定卸载目录
torch_dtype=torch.bfloat16,
trust_remote_code=True
)
# 手动初始化 meta 张量
model = model.to_empty(device="cuda") # 先分配空内存
model.load_state_dict(model.state_dict()) # 重新加载数据
model.enable_input_require_grads() # 开启梯度检查点时,要执行该方法
LoRA配置
- 使用LoRA(低秩适应)进行高效微调
- 配置参数:
- 目标模块:包括注意力投影层和前馈网络层
- 秩(r)=8,alpha=32,dropout=0.1。alpha为低秩矩阵的缩放系数,用于调整适配器(Adapter)输出的权重大小。其作用类似于学习率,数值越大,低秩矩阵对原始模型参数的修正幅度越大。实际缩放因子: 32/8 =4 意味着LoRA的更新量会被放大4倍。
- 任务类型为因果语言模型(CAUSAL_LM)
config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
inference_mode=False, # 训练模式
r=8, # Lora 秩
lora_alpha=32,
lora_dropout=0.1,
)
model = get_peft_model(model, config)
4、训练配置
训练参数(TrainingArguments):
- 输出目录:
./output/Qwen2-NER - 批次大小:每个设备4个样本,梯度累积步数4
- 训练轮数:2个epoch
- 学习率:1e-4
- 启用梯度检查点以节省内存
- 日志和保存步数配置
args = TrainingArguments(
output_dir="./output/Qwen2-NER",
per_device_train_batch_size=4,
per_device_eval_batch_size=4,
gradient_accumulation_steps=4,
logging_steps=10,
num_train_epochs=2,
save_steps=100,
learning_rate=1e-4,
save_on_each_node=True,
gradient_checkpointing=True,
report_to="none",
)
5、训练与评估
训练过程
- 创建Trainer实例,配置模型、参数、数据集和数据整理器
- 调用
trainer.train()开始训练
输出目录:output/Qwen2-NER,包括以下LoRA模块文件:
- adapter_model.bin
- training_args.bin
- adapter_config.json
微调训练后的配置信息如下,可以看出: “peft_type”的值为 “LORA”。
{
"alpha_pattern": {},
"auto_mapping": null,
"base_model_name_or_path": "Qwen/Qwen2.5-1.5B-Instruct",
"bias": "none",
"corda_config": null,
"eva_config": null,
"exclude_modules": null,
"fan_in_fan_out": false,
"inference_mode": true,
"init_lora_weights": true,
"layer_replication": null,
"layers_pattern": null,
"layers_to_transform": null,
"loftq_config": {},
"lora_alpha": 32,
"lora_bias": false,
"lora_dropout": 0.1,
"megatron_config": null,
"megatron_core": "megatron.core",
"modules_to_save": null,
"peft_type": "LORA",
"qalora_group_size": 16,
"r": 8,
"rank_pattern": {},
"revision": null,
"target_modules": [
"down_proj",
"o_proj",
"q_proj",
"gate_proj",
"v_proj",
"k_proj",
"up_proj"
],
"target_parameters": null,
"task_type": "CAUSAL_LM",
"trainable_token_indices": null,
"use_dora": false,
"use_qalora": false,
"use_rslora": false
}
测试评估
- 从数据集中随机选取20条测试样本
- 使用
predict函数进行预测:- 应用聊天模板格式化输入
- 生成模型输出(max_new_tokens=512)
- 解码并返回响应
def predict(messages, model, tokenizer):
device = "cuda"
text = tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True
)
model_inputs = tokenizer([text], return_tensors="pt").to(device)
generated_ids = model.generate(
model_inputs.input_ids,
max_new_tokens=512
)
generated_ids = [
output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)
]
response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
print(response)
return response
运行展示
python train_qwen2.5_ner.py
Downloading Model from https://www.modelscope.cn to directory: C:\Users\admin\.cache\modelscope\hub\models\Qwen\Qwen2.5-1.5B-Instruct for C:\Users\admin\.cache\modelscope\hub\models\Qwen\Qwen2___5-1___5B-Instruct.
Sliding Window Attention is enabled but not implemented for `sdpa`; unexpected results may be encountered.
Some parameters are on the meta device because they were offloaded to the cpu.
Map: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████| 14152/14152 [00:32<00:00, 439.50 examples/s]
No label_names provided for model class `PeftModelForCausalLM`. Since `PeftModel` hides base models input arguments, if label_names is not given, label_names can't be set automatically within `Trainer`. Note that empty label_names list will be used instead.
0%| | 0/1768 [00:00<?, ?it/s]`use_cache=True` is incompatible with gradient checkpointing. Setting `use_cache=False`.
{'loss': 11.9312, 'grad_norm': 0.0, 'learning_rate': 9.94343891402715e-05, 'epoch': 0.01}
{'loss': 11.9312, 'grad_norm': 0.0, 'learning_rate': 9.8868778280543e-05, 'epoch': 0.02}
{'loss': 11.9312, 'grad_norm': 0.0, 'learning_rate': 9.830316742081447e-05, 'epoch': 0.03}
{'loss': 11.9312, 'grad_norm': 0.0, 'learning_rate': 9.773755656108597e-05, 'epoch': 0.05}
3%|██▊ | 45/1768 [3:07:11<78:23:38, 163.80s/it]
大模型在NER任务微调后的目标举例:
你是一个文本实体识别领域的专家,你需要从给定的句子中提取:
地点; 人名; 地理实体; 组织实体。
以 json 格式输出, 如 {"entity_text": "南京", "entity_label": "地理实体"}
注意:
1. 输出的每一行都必须是正确的 json 字符串.
2. 找不到任何实体时, 输出"没有找到任何实体".
句子为:菲律宾总统埃斯特拉达2号透过马尼拉当地电台宣布说,
在仍遭到激进的回教阿卜沙耶夫组织羁押在非国南部和落岛的16名人质当中,
军方已经营救出了11名菲律宾人质。
输出:
{"entity_text": "菲律宾", "entity_label": "地理实体"}
{"entity_text": "埃斯特拉达", "entity_label": "人名"}
{"entity_text": "马尼拉", "entity_label": "地理实体"}
{"entity_text": "阿卜沙耶夫组织", "entity_label": "组织"}
{"entity_text": "非国南部", "entity_label": "地理实体"}
运行 python train_qwen2.5_ner.py 的部分输出如下:
Sliding Window Attention is enabled but not implemented for `sdpa`; unexpected results may be encountered.
Map: 100%|█████████████████████████████████████████████████████████████████| 1800/1800 [00:07<00:00, 252.30 examples/s]
No label_names provided for model class `PeftModelForCausalLM`. Since `PeftModel` hides base models input arguments, if label_names is not given, label_names can't be set automatically within `Trainer`. Note that empty label_names list will be used instead.
0%| | 0/224 [00:00<?, ?it/s]`use_cache=True` is incompatible with gradient checkpointing. Setting `use_cache=False`.
{'loss': 10.1305, 'grad_norm': 14.249238014221191, 'learning_rate': 9.598214285714287e-05, 'epoch': 0.09}
{'loss': 5.9022, 'grad_norm': 7.508850574493408, 'learning_rate': 9.151785714285715e-05, 'epoch': 0.18}
{'loss': 4.7459, 'grad_norm': 3.835057497024536, 'learning_rate': 8.705357142857143e-05, 'epoch': 0.27}
{'loss': 4.0324, 'grad_norm': 2.720217227935791, 'learning_rate': 8.258928571428572e-05, 'epoch': 0.36}
{'loss': 3.782, 'grad_norm': 2.55788254737854, 'learning_rate': 7.8125e-05, 'epoch': 0.44}
{'loss': 3.6743, 'grad_norm': 1.6443232297897339, 'learning_rate': 7.36607142857143e-05, 'epoch': 0.53}
{'loss': 3.6023, 'grad_norm': 1.526658058166504, 'learning_rate': 6.919642857142857e-05, 'epoch': 0.62}
{'loss': 3.5268, 'grad_norm': 1.2773854732513428, 'learning_rate': 6.473214285714287e-05, 'epoch': 0.71}
{'loss': 3.5583, 'grad_norm': 2.4730019569396973, 'learning_rate': 6.026785714285714e-05, 'epoch': 0.8}
{'loss': 3.3528, 'grad_norm': 2.0641303062438965, 'learning_rate': 5.5803571428571434e-05, 'epoch': 0.89}
{'loss': 3.0723, 'grad_norm': 3.2526516914367676, 'learning_rate': 5.133928571428571e-05, 'epoch': 0.98}
{'loss': 2.4782, 'grad_norm': 42.267974853515625, 'learning_rate': 4.6875e-05, 'epoch': 1.06}
{'loss': 2.1806, 'grad_norm': 8.70439624786377, 'learning_rate': 4.2410714285714285e-05, 'epoch': 1.15}
{'loss': 1.8771, 'grad_norm': 25.65455436706543, 'learning_rate': 3.794642857142857e-05, 'epoch': 1.24}
{'loss': 1.7779, 'grad_norm': 6.981565952301025, 'learning_rate': 3.348214285714286e-05, 'epoch': 1.33}
{'loss': 1.5114, 'grad_norm': 7.910628318786621, 'learning_rate': 2.9017857142857146e-05, 'epoch': 1.42}
{'loss': 1.4393, 'grad_norm': 7.130474090576172, 'learning_rate': 2.455357142857143e-05, 'epoch': 1.51}
{'loss': 1.3028, 'grad_norm': 8.382883071899414, 'learning_rate': 2.0089285714285717e-05, 'epoch': 1.6}
{'loss': 1.3351, 'grad_norm': 5.515603065490723, 'learning_rate': 1.5625e-05, 'epoch': 1.68}
{'loss': 1.2376, 'grad_norm': 5.291408538818359, 'learning_rate': 1.1160714285714287e-05, 'epoch': 1.77}
{'loss': 1.1605, 'grad_norm': 5.604613304138184, 'learning_rate': 6.696428571428572e-06, 'epoch': 1.86}
{'loss': 1.1743, 'grad_norm': 5.753052711486816, 'learning_rate': 2.2321428571428573e-06, 'epoch': 1.95}
{'train_runtime': 28535.9277, 'train_samples_per_second': 0.126, 'train_steps_per_second': 0.008, 'train_loss': 3.008452915719577, 'epoch': 1.99}
100%|██████████████████████████████████████████████████████████████████████████████████████████| 224/224 [7:55:35<00:00, 127.39s/it]
训练完成!开始测试模型…
{"entity_text": "台北", "entity_label": "组织"}{"entity_text": "组织"}entity_label": "地理名"}entity_text": "地理韩", "entity_label": "地理实体"}{"entity_text": "中国", "entity_label": "地理实体"} "美国":", "任何_label": "entity_label": "地理实体"}", "日本_label": "entity_text"} "地理":"}行斯":实体", "entity_label": "地理实体"}entity_text": "entity_text": "地理":{"尔", "entity_label": "地理实体"}{"entity_text": "中共", "entity_label": "地理实体"}{"entity_text": "日本克", "entity_label": "人名"}{"entity_text": "地点", "entity_label": "地理实体"}entity_text": "香港", "entity_label": "地理实体"}entity_text": "南", "entity_label": "人实体"}entity_text": "组织"}", "entity_label": "人"}{"entity_text": "台湾", "entity_label": "地理实体"}{"entity_text": "美国亚", "entity_label": "地理实体"} "高":", "院_label": "entity"}": "大陆"}": "entity_text": "俄罗斯"}{"entity_text": "地理", "entity_label": "组织名"}{"entity_text": "印度", "entity_label": "地理_label":{"", "entity_label": "人实体"} "任何":", "entity_label": "组织"}{"entity_text": "组织"}", "entity_label": "地点名"} "entity_text": "人金":", "entity_label": "人名"}{"entity_text": "台湾", "entity_label": "地理实体"}entity_text": "台湾", "entity_label": "地理实体"}entity_text": "地点", "entity_label": "地理实体"}{"entity_text": "尼", "entity_label": "地理实体"}{"entity_text": "香港", "entity_label": "组织"}"} "大 "": "": "组织"}": "实体"}": "地理": "entity_label": "地理":"}", "entity_text": "组织"}军", "entity_label": "地点名"} "entity_text": "人", "entity_label": "地理实体"}entity_text": "entity "北 "地理":":": "实体
{"entity_text": "组织", "entity_label": "地理实体"}{"entity_text": "美国", "entity_label": "组织"}{"entity_text": "中共亚", "entity_label": "组织"}entity_text": "地理实体"}entity_text": "组织"}"} "entity_text": "地点名"}{"entity_text": "entity_text": "地理名"}entity_text": "地点金":{"entity_text": "人", "entity_label": "人实体"}{"entity_text": "地理":", "entity_label": "人名"}{"", "entity_label": "地理实体"}{"entity_text": "俄罗斯", "entity_label": "地理实体"}entity_text": "台湾", "entity_label": "地理实体"}entity_text": "中共任何德", "entity_label": "人名"}{"entity_text": "香港", "entity_label": "地理实体"}entity_text": "越南", "entity_label": "地理实体"}entity_text": "组织", "entity_label": "组织"}{"entity_text": "组织", "entity_label": "地理实体"} "人":", "组织_label": "entity_text"} "地理", "entity_label": "人实体"}entity_text": "北", "entity_label": "组织"}{"entity_text": "美国", "entity_label": "人实体"}entity_text": "entity_text": "组织"}人":": "组织", "entity_label": "地理实体"} "entity"}", "克_label": "entity_text": "找到", "entity_label": "人实体"}{"entity_text": "中共", "entity_label": "地理实体"}entity_text": "台湾", "entity_label": "{"实体"}", "entity_label": "组织"}":{"", "entity_label": "地理实体"}{"entity_text": "中共", "entity_label": "组织"}{"entity_text": "组织布", "entity_label": "人名"}{"entity_text": "entity_text": "台北_label": "entity_text": "人_text":{"找到", "entity_label": "entity"}", "组织_label": "entity"}": "人_text":{"尔", "entity_label": "地理实体"}{"entity_text": "香港"}entity_text": "entity_text": "entity_text": "组织", "entity_label": "组织"}":", "entity_label": "
我偶尔阅读 出行博客。增长见识看到照片。 聖特雷莎坡道 照片令人惊艳。敬意 独创性。
我珍视, 充满真情实感。你的内容 就是 这样的。干得好。 日落航行 我尊重这样的项目, 这里展示真正的旅游。你的项目 就是 属于这里的。干得好。
实用的 旅游网站! 越来越棒! 热带温室穹顶 美好的 国家随笔! 把国家加入清单。
我热爱 旅行页面。太棒了掌握出行细节。 拱廊柱廳 我很少遇到, 如此温暖又有信息量的博客。非常酷。
欣赏你的照片, 我感受到, 旅游让人相连。感谢 美好的心情。 霍雨林 令人惊叹的 旅行者门户网站, 继续发展 保持这种风格。衷心感谢!
关注更新, 我明白, 旅行让人焕然一新。谢谢 情感。 什刹海相連 不常看到, 这么高质量的内容。太棒了。