【大模型微调系列-02】 深度学习与大模型初识

发布于:2025-08-16 ⋅ 阅读:(13) ⋅ 点赞:(0)

【大模型微调系列-02】 深度学习与大模型初识

在第1章中,我们已经搭建好了开发环境,安装了必要的工具和库。现在,让我们正式踏入深度学习和大模型的世界。本章将用最直观的方式,带你理解神经网络的基本原理、大模型的特点,并通过实际操作体验Qwen模型的强大能力。

2.1 理论讲解:从神经元到大模型

2.1.1 神经网络基本原理

生物启发:从大脑到人工智能

想象一下你的大脑是如何识别一只猫的。当你看到一只猫时,视觉信号通过眼睛传入大脑,经过无数个神经元的处理和传递,最终你的大脑告诉你:"这是一只猫!"人工神经网络就是模仿这个过程设计的。

在生物神经元中,树突接收信号,细胞体处理信号,轴突传递信号。人工神经元的工作原理类似:

权重 w1
权重 w2
权重 w3
输入1 x1
加权求和
输入2 x2
输入3 x3
加偏置 +b
激活函数
输出 y

让我们用一个生活化的例子来理解:假设你要决定今天是否出门跑步,你会考虑三个因素:

  • 天气(输入1):晴天得3分,阴天得1分,雨天得-2分
  • 心情(输入2):好心情得2分,一般得0分,差得-1分
  • 时间(输入3):充裕得2分,紧张得-1分

每个因素的重要性不同(权重):

  • 天气权重:0.5(比较重要)
  • 心情权重:0.3(一般重要)
  • 时间权重:0.2(相对次要)

最终得分 = 天气×0.5 + 心情×0.3 + 时间×0.2 + 偏置值

如果最终得分超过某个阈值(比如1分),你就出门跑步,否则待在家里。这个"阈值判断"就是激活函数的作用。

激活函数:神经网络的"开关门"

激活函数就像一扇智能门,决定信号是否通过、如何通过。没有激活函数,神经网络只能学习线性关系,就像只能画直线,无法画曲线。

常见的激活函数有:

  • ReLU:像个单向阀门,负数归零,正数保持
  • Sigmoid:像个压缩器,把任何值压缩到0-1之间
  • Tanh:类似Sigmoid,但压缩到-1到1之间
层结构:深度学习的"流水线工厂"

神经网络的层结构就像一个流水线工厂,每层负责不同的加工任务:

输出层:物体分类
隐藏层2:形状识别
隐藏层1:边缘检测
输入层
猫 85%
狗 10%
兔 5%
识别圆形
识别方形
识别三角
检测横线
检测竖线
检测曲线
像素1
像素2
像素3
...
  • 输入层:接收原始数据(如图片的像素值)
  • 隐藏层1:提取低级特征(如边缘、纹理)
  • 隐藏层2:组合低级特征成高级特征(如形状、部件)
  • 输出层:做出最终判断(这是什么物体)

层数越多,网络越"深",这就是"深度学习"名称的由来。

2.1.2 模型规模化:从小到大的进化

参数量:神经网络的"乐高积木"

想象你在玩乐高积木。如果只有100块积木,你可能只能搭建一个简单的小房子。但如果有10万块积木,你就能搭建一座精致的城堡,甚至是一个迷你城市。

神经网络的参数就像乐高积木:

  • 参数越多:能学习的模式越复杂,表达能力越强
  • 参数越少:学习能力有限,只能处理简单任务

让我们看看不同规模模型的对比:

模型规模 参数量级 典型代表 能力特点 应用场景
微型模型 <100M BERT-Tiny 基础文本分类
简单模式识别 移动端应用
实时响应场景
小型模型 100M-1B BERT-Base
GPT-2 Small 理解语义
简单对话 轻量级助手
文本分析
中型模型 1B-10B Qwen-1.8B
ChatGLM-6B 复杂推理
多轮对话 智能客服
内容创作
大型模型 10B-100B Qwen-14B
LLaMA-30B 深度理解
创意生成 专业咨询
代码生成
超大型模型 >100B GPT-4
Qwen-72B 近人类水平
跨领域能力 通用AI助手
复杂决策
为什么规模很重要?

让我们通过一个具体例子来理解:

任务:理解"今天天气不错,我们去爬山吧"

  • 100M模型:识别出"天气"、"爬山"等关键词
  • 1B模型:理解这是一个建议,涉及户外活动
  • 7B模型:推断出说话人心情好,想要户外运动,可能需要准备登山装备
  • 70B模型:考虑季节、地理位置、体力要求,给出详细的爬山建议和注意事项
模型能力边界
1亿参数
10亿参数
70亿参数
700亿参数
千亿参数
语法分析
词汇理解
语义理解
逻辑推理
创意生成
通用智能

2.1.3 大模型应用场景

大模型就像一个博学的助手,可以在多个领域发挥作用:

1. 文本生成:你的创意伙伴
  • 营销文案:“为新款运动鞋写一段吸引年轻人的广告语”
  • 故事创作:“续写一个关于未来城市的科幻故事”
  • 邮件撰写:“帮我写一封礼貌的会议延期通知”
2. 对话系统:全天候助理
  • 客服机器人:回答产品咨询、处理售后问题
  • 学习伴侣:解答作业疑问、提供学习建议
  • 心理陪伴:倾听烦恼、给予情感支持
3. 代码生成:程序员的好帮手
  • 自动补全:根据注释生成函数实现
  • Bug修复:分析错误信息,提供修复方案
  • 代码优化:重构代码,提升性能
4. 知识问答:移动的百科全书
  • 专业咨询:“解释量子计算的基本原理”
  • 生活指导:“如何制作正宗的麻婆豆腐”
  • 学术研究:“总结深度学习最新进展”
5. 内容理解:智能分析师
  • 文档摘要:提取长文档的核心要点
  • 情感分析:判断评论的正面/负面倾向
  • 信息抽取:从文本中提取人名、地点、事件
6. 创意设计:灵感激发器
  • 标题生成:为文章起一个吸引人的标题
  • 方案策划:设计活动流程、营销方案
  • 头脑风暴:提供多角度的创意思路

不同应用对模型的要求:

应用场景 所需能力 推荐模型规模 响应时间要求 准确度要求
实时聊天 流畅对话 1B-7B <1秒 中等
专业咨询 深度知识 7B-14B <5秒
创意写作 想象力 7B-72B <10秒 中等
代码生成 逻辑严谨 7B-34B <5秒 极高
文档分析 理解力 3B-14B <3秒

2.2 实操案例:与Qwen模型的第一次亲密接触

现在,让我们通过实际操作来体验大模型的魅力。请确保你已经按照第1章完成了环境配置。

环境准备与检查

首先,让我们确保所有依赖都已正确安装:

# 环境检查脚本 - 请在开始实验前运行
import sys
import torch
import transformers
import warnings
warnings.filterwarnings('ignore')

def check_environment():
    """
    检查并显示当前环境配置
    这个函数会告诉你环境是否准备就绪
    """
    print("="*50)
    print("🔍 环境检查开始...")
    print("="*50)
    
    # 检查Python版本
    python_version = sys.version
    print(f"✅ Python版本: {python_version.split()[0]}")
    if sys.version_info < (3, 8):
        print("⚠️  警告: 建议使用Python 3.8或更高版本")
    
    # 检查PyTorch
    try:
        torch_version = torch.__version__
        print(f"✅ PyTorch版本: {torch_version}")
        
        # 检查CUDA可用性
        if torch.cuda.is_available():
            print(f"✅ CUDA可用: {torch.cuda.get_device_name(0)}")
            print(f"   显存大小: {torch.cuda.get_device_properties(0).total_memory / 1024**3:.1f} GB")
        else:
            print("⚠️  CUDA不可用,将使用CPU运行(速度较慢)")
    except ImportError:
        print("❌ PyTorch未安装!请运行: pip install torch")
        return False
    
    # 检查Transformers
    try:
        trans_version = transformers.__version__
        print(f"✅ Transformers版本: {trans_version}")
    except ImportError:
        print("❌ Transformers未安装!请运行: pip install transformers")
        return False
    
    # 检查其他依赖
    required_packages = {
        'numpy': 'numpy',
        'tqdm': 'tqdm',
        'accelerate': 'accelerate'
    }
    
    for package_name, import_name in required_packages.items():
        try:
            __import__(import_name)
            print(f"✅ {package_name}已安装")
        except ImportError:
            print(f"⚠️  {package_name}未安装,建议安装: pip install {package_name}")
    
    print("="*50)
    print("✨ 环境检查完成!")
    print("="*50)
    return True

# 运行环境检查
if check_environment():
    print("\n🎉 太好了!你的环境已经准备就绪,让我们开始探索大模型吧!")
else:
    print("\n⚠️  请先安装缺失的依赖包再继续")

如果运行出现问题,请检查以下常见原因:

  1. 内存不足:确保有至少16GB RAM
  2. 包版本冲突:运行 pip install --upgrade transformers torch
  3. 网络问题:使用国内镜像源安装包

案例1:加载Qwen模型初体验

现在让我们正式与Qwen模型见面!

from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
import time

def load_qwen_model(model_name="Qwen/Qwen-1_8B-Chat", device_map="auto"):
    """
    加载Qwen模型
    
    参数:
        model_name: 模型名称,可选 Qwen-1_8B-Chat, Qwen-7B-Chat等
        device_map: 设备映射,"auto"会自动选择GPU或CPU
    
    返回:
        model: 加载好的模型
        tokenizer: 对应的分词器
    """
    print(f"🚀 开始加载模型: {model_name}")
    print("⏳ 首次加载需要下载模型,请耐心等待...")
    
    start_time = time.time()
    
    try:
        # 加载分词器(将文本转换为模型能理解的数字)
        tokenizer = AutoTokenizer.from_pretrained(
            model_name,
            trust_remote_code=True,  # Qwen模型需要这个参数
            cache_dir="./model_cache"  # 指定缓存目录,避免重复下载
        )
        
        # 加载模型
        model = AutoModelForCausalLM.from_pretrained(
            model_name,
            torch_dtype=torch.float16,  # 使用半精度,节省内存
            device_map=device_map,
            trust_remote_code=True,
            cache_dir="./model_cache"
        )
        
        elapsed_time = time.time() - start_time
        print(f"✅ 模型加载成功!用时: {elapsed_time:.1f}秒")
        
        # 显示模型信息
        total_params = sum(p.numel() for p in model.parameters())
        print(f"📊 模型参数量: {total_params/1e9:.1f}B (十亿)")
        
        # 检查内存使用
        if torch.cuda.is_available():
            memory_used = torch.cuda.memory_allocated() / 1024**3
            print(f"💾 GPU显存占用: {memory_used:.1f}GB")
        
        return model, tokenizer
        
    except Exception as e:
        print(f"❌ 模型加载失败: {str(e)}")
        print("\n💡 解决建议:")
        print("1. 检查网络连接,确保能访问HuggingFace")
        print("2. 如果内存不足,尝试使用更小的模型")
        print("3. 使用国内镜像: export HF_ENDPOINT=https://hf-mirror.com")
        return None, None

# 加载模型(根据你的硬件选择合适的模型)
# 内存小于8GB: 使用 Qwen/Qwen-1_8B-Chat
# 内存8-16GB: 使用 Qwen/Qwen-7B-Chat
# 内存>16GB: 可以尝试 Qwen/Qwen-14B-Chat

model, tokenizer = load_qwen_model("Qwen/Qwen-1_8B-Chat")
基础对话:打个招呼
def chat_with_qwen(model, tokenizer, prompt, max_length=512):
    """
    与Qwen模型对话
    
    参数:
        prompt: 用户输入的问题
        max_length: 最大生成长度
    
    返回:
        response: 模型的回复
    """
    print(f"\n👤 用户: {prompt}")
    print("🤖 Qwen正在思考...")
    
    # 将输入转换为模型能理解的格式
    inputs = tokenizer(prompt, return_tensors="pt")
    
    # 如果有GPU,将输入移到GPU上
    if torch.cuda.is_available():
        inputs = inputs.to("cuda")
    
    # 生成回复
    with torch.no_grad():  # 不计算梯度,节省内存
        outputs = model.generate(
            inputs.input_ids,
            max_length=max_length,
            num_return_sequences=1,
            temperature=0.7,  # 控制随机性,越高越随机
            do_sample=True,   # 启用采样,让回复更自然
            top_p=0.95,      # 核采样,只考虑概率最高的词
            pad_token_id=tokenizer.eos_token_id,
            use_cache=False
        )
    
    # 解码输出(将数字转回文本)
    response = tokenizer.decode(outputs[0], skip_special_tokens=True)
    
    # 移除输入部分,只保留回复
    response = response.replace(prompt, "").strip()
    
    print(f"🤖 Qwen: {response}")
    return response

# 示例1:简单问答
if model is not None:
    print("\n" + "="*50)
    print("📝 示例1: 基础问答")
    print("="*50)

    print(tokenizer)
    
    response1 = chat_with_qwen(
        model, tokenizer,
        "你好!请简单介绍一下你自己。"
    )
进阶任务:文本续写
def text_continuation(model, tokenizer, start_text, max_new_tokens=200):
    """
    文本续写功能
    给定开头,让模型续写故事
    """
    print(f"\n📖 故事开头: {start_text}")
    print("✍️  续写中...")
    
    inputs = tokenizer(start_text, return_tensors="pt")
    
    if torch.cuda.is_available():
        inputs = inputs.to("cuda")
    
    # 使用更高的temperature让故事更有创意
    with torch.no_grad():
        outputs = model.generate(
            inputs.input_ids,
            max_new_tokens=max_new_tokens,  # 生成的新token数量
            temperature=0.9,  # 提高创造性
            do_sample=True,
            top_p=0.95,
            repetition_penalty=1.2,  # 避免重复
            pad_token_id=tokenizer.eos_token_id,
            use_cache=False
        )
    
    story = tokenizer.decode(outputs[0], skip_special_tokens=True)
    continuation = story.replace(start_text, "").strip()
    
    print(f"\n📚 完整故事:")
    print(f"{start_text}{continuation}")
    
    return continuation

# 示例2:创意续写
if model is not None:
    print("\n" + "="*50)
    print("📝 示例2: 故事续写")
    print("="*50)
    
    story_start = "从前有座山,山上有座庙,庙里有个老和尚。有一天,老和尚发现了一本神秘的古书,"
    
    continuation = text_continuation(
        model, tokenizer,
        story_start,
        max_new_tokens=150
    )
应用实例:角色扮演
def role_play_chat(model, tokenizer, role, question):
    """
    让模型扮演特定角色回答问题
    
    参数:
        role: 角色设定
        question: 用户的问题
    """
    # 构建角色扮演的提示词
    prompt = f"""你现在是一个{role}。请以这个角色的身份、语气和知识水平来回答问题。

用户问题: {question}

{role}的回答:"""
    
    print(f"\n🎭 角色设定: {role}")
    print(f"❓ 问题: {question}")
    print(f"💭 {role}正在思考...")
    
    inputs = tokenizer(prompt, return_tensors="pt")
    
    if torch.cuda.is_available():
        inputs = inputs.to("cuda")
    
    with torch.no_grad():
        outputs = model.generate(
            inputs.input_ids,
            max_length=512,
            temperature=0.8,
            do_sample=True,
            top_p=0.95,
            pad_token_id=tokenizer.eos_token_id,
            use_cache=False
        )
    
    response = tokenizer.decode(outputs[0], skip_special_tokens=True)
    answer = response.split(f"{role}的回答:")[-1].strip()
    
    print(f"🎭 {role}: {answer}")
    return answer

# 示例3:角色扮演对话
if model is not None:
    print("\n" + "="*50)
    print("📝 示例3: 角色扮演")
    print("="*50)
    
    # 测试不同角色
    roles_and_questions = [
        ("Python老师", "什么是列表推导式?请举个例子"),
        ("健身教练", "如何快速增肌?"),
        ("美食评论家", "如何评价麻辣火锅?")
    ]
    
    for role, question in roles_and_questions:
        answer = role_play_chat(model, tokenizer, role, question)
        print()  # 空行分隔

案例2:参数调节实验

让我们通过调整参数来观察模型行为的变化:

def parameter_experiment(model, tokenizer, prompt, experiments):
    """
    参数调节实验
    通过调整不同参数,观察输出的变化
    
    参数:
        prompt: 输入提示
        experiments: 实验配置列表
    """
    print(f"\n🔬 参数实验: {prompt}")
    print("="*60)
    
    results = []
    
    for exp_name, params in experiments:
        print(f"\n实验: {exp_name}")
        print(f"参数: temperature={params['temperature']}, "
              f"top_p={params['top_p']}, "
              f"max_length={params['max_length']}")
        
        inputs = tokenizer(prompt, return_tensors="pt")
        
        if torch.cuda.is_available():
            inputs = inputs.to("cuda")
        
        with torch.no_grad():
            outputs = model.generate(
                inputs.input_ids,
                **params,
                do_sample=True,
                pad_token_id=tokenizer.eos_token_id,
                use_cache=False
            )
        
        response = tokenizer.decode(outputs[0], skip_special_tokens=True)
        response = response.replace(prompt, "").strip()
        
        print(f"输出: {response[:200]}...")  # 只显示前200字符
        
        results.append({
            "实验名称": exp_name,
            "temperature": params['temperature'],
            "top_p": params['top_p'],
            "输出长度": len(response),
            "输出预览": response[:100] + "..."
        })
    
    return results

# 定义实验参数
if model is not None:
    print("\n" + "="*50)
    print("🧪 参数调节实验")
    print("="*50)
    
    test_prompt = "人工智能的未来发展方向是"
    
    experiments = [
        ("保守模式", {"temperature": 0.3, "top_p": 0.9, "max_length": 100}),
        ("平衡模式", {"temperature": 0.7, "top_p": 0.95, "max_length": 100}),
        ("创意模式", {"temperature": 1.2, "top_p": 0.98, "max_length": 100})
    ]
    
    results = parameter_experiment(model, tokenizer, test_prompt, experiments)
    
    # 创建对比表格
    print("\n📊 参数影响对比:")
    print("-" * 80)
    print(f"{'实验名称':<12} {'Temperature':<12} {'Top-p':<8} {'输出长度':<10} {'特点'}")
    print("-" * 80)
    
    for r in results:
        # 分析输出特点
        if r['temperature'] < 0.5:
            特点 = "保守稳定,逻辑严谨"
        elif r['temperature'] < 0.9:
            特点 = "平衡自然,可读性好"
        else:
            特点 = "创意丰富,可能失控"
        
        print(f"{r['实验名称']:<12} {r['temperature']:<12.1f} {r['top_p']:<8.2f} "
              f"{r['输出长度']:<10} {特点}")

案例3:模型能力测试

让我们设计一组测试来全面评估模型的能力:

import time

def comprehensive_test(model, tokenizer):
    """
    全面测试模型的各项能力
    包括:创意写作、逻辑推理、代码生成、中文理解、知识问答
    """
    print("\n" + "="*60)
    print("🎯 模型能力综合测试")
    print("="*60)
    
    test_cases = [
        {
            "能力": "创意写作",
            "prompt": "写一个关于AI觉醒的微型科幻故事,不超过100字。",
            "评分标准": "创意性、情节完整性"
        },
        {
            "能力": "逻辑推理",
            "prompt": "小明比小红高,小红比小李高,请问谁最矮?",
            "评分标准": "逻辑正确性"
        },
        {
            "能力": "代码生成",
            "prompt": "用Python写一个函数,计算斐波那契数列的第n项。",
            "评分标准": "代码正确性、可读性"
        },
        {
            "能力": "中文理解",
            "prompt": "解释成语'画蛇添足'的含义,并举一个生活中的例子。",
            "评分标准": "理解准确性、例子贴切性"
        },
        {
            "能力": "知识问答",
            "prompt": "简述机器学习和深度学习的区别。",
            "评分标准": "准确性、完整性"
        }
    ]
    
    results = []
    
    for i, test in enumerate(test_cases, 1):
        print(f"\n测试 {i}: {test['能力']}")
        print(f"题目: {test['prompt']}")
        print("回答: ", end="")
        
        start_time = time.time()
        
        inputs = tokenizer(test['prompt'], return_tensors="pt")
        if torch.cuda.is_available():
            inputs = inputs.to("cuda")
        
        with torch.no_grad():
            outputs = model.generate(
                inputs.input_ids,
                max_length=256,
                temperature=0.7,
                do_sample=True,
                top_p=0.95,
                pad_token_id=tokenizer.eos_token_id,
                use_cache=False
            )
        
        response = tokenizer.decode(outputs[0], skip_special_tokens=True)
        response = response.replace(test['prompt'], "").strip()
        
        response_time = time.time() - start_time
        
        print(response[:300])  # 限制显示长度
        print(f"⏱️ 响应时间: {response_time:.2f}秒")
        
        results.append({
            "能力": test['能力'],
            "响应时间": response_time,
            "回答质量": "待人工评估"  # 实际应用中可以加入自动评分
        })
    
    # 生成测试报告
    print("\n" + "="*60)
    print("📈 测试报告总结")
    print("="*60)
    
    total_time = sum(r['响应时间'] for r in results)
    avg_time = total_time / len(results)
    
    print(f"✅ 完成测试项目: {len(results)}项")
    print(f"⏱️ 平均响应时间: {avg_time:.2f}秒")
    print(f"💾 模型规模: 1.8B参数")
    
    print("\n📊 各项能力表现:")
    print("-" * 40)
    for r in results:
        print(f"{r['能力']:<10} 响应时间: {r['响应时间']:.2f}秒")
    
    return results

# 运行综合测试
if model is not None:
    test_results = comprehensive_test(model, tokenizer)

批量测试脚本

为了方便扩展测试,这里提供一个批量测试框架:

def batch_test_framework(model, tokenizer, test_file=None):
    """
    批量测试框架
    可以从文件读取测试用例,或使用默认测试集
    
    参数:
        test_file: 测试用例文件路径(JSON格式)
    """
    import json
    
    # 默认测试集
    default_tests = [
        {"category": "翻译", "input": "Hello, how are you?", "expected_contains": ["你好", "您好"]},
        {"category": "总结", "input": "请用一句话总结:深度学习是机器学习的一个分支,它使用多层神经网络来学习数据的表示。", "expected_contains": ["神经网络", "学习"]},
        {"category": "问答", "input": "中国的首都是哪里?", "expected_contains": ["北京"]},
    ]
    
    # 如果提供了测试文件,从文件加载
    if test_file:
        try:
            with open(test_file, 'r', encoding='utf-8') as f:
                tests = json.load(f)
        except:
            print("⚠️ 无法加载测试文件,使用默认测试集")
            tests = default_tests
    else:
        tests = default_tests
    
    print("\n🚀 开始批量测试")
    print("="*60)
    
    passed = 0
    failed = 0
    
    for i, test in enumerate(tests, 1):
        print(f"\n测试 #{i} - {test['category']}")
        print(f"输入: {test['input']}")
        
        # 生成回复
        inputs = tokenizer(test['input'], return_tensors="pt")
        if torch.cuda.is_available():
            inputs = inputs.to("cuda")
        
        with torch.no_grad():
            outputs = model.generate(
                inputs.input_ids,
                max_length=256,
                temperature=0.7,
                do_sample=True,
                top_p=0.95,
                pad_token_id=tokenizer.eos_token_id,
                use_cache=False
            )
        
        response = tokenizer.decode(outputs[0], skip_special_tokens=True)
        response = response.replace(test['input'], "").strip()
        
        print(f"输出: {response[:200]}")
        
        # 检查预期内容
        if 'expected_contains' in test:
            contains_expected = any(keyword in response for keyword in test['expected_contains'])
            if contains_expected:
                print("✅ 测试通过")
                passed += 1
            else:
                print("❌ 测试失败:未包含预期关键词")
                failed += 1
        else:
            print("⏭️ 跳过验证(无预期结果)")
    
    # 测试总结
    print("\n" + "="*60)
    print("📊 测试总结")
    print(f"✅ 通过: {passed}")
    print(f"❌ 失败: {failed}")
    print(f"⏭️ 未验证: {len(tests) - passed - failed}")
    print(f"🎯 通过率: {passed/len(tests)*100:.1f}%")

# 运行批量测试
if model is not None:
    batch_test_framework(model, tokenizer)

2.3 本章小结

通过本章的学习,我们已经:

🎯 掌握的知识点

  1. 理解了神经网络的基本原理

    • 神经元如何处理信息
    • 激活函数的作用
    • 深度网络的层次结构
  2. 认识了大模型的特点

    • 参数规模与能力的关系
    • 不同规模模型的应用场景
    • 大模型的优势与局限
  3. 体验了Qwen模型的实际能力

    • 基础对话与问答
    • 创意文本生成
    • 角色扮演与任务完成
  4. 学会了参数调节

    • Temperature控制创造性
    • Top-p影响多样性
    • 不同参数组合的效果

网站公告

今日签到

点亮在社区的每一天
去签到