transformer

transformer

1 transformer介绍

  • 概念
    • transformer是基于自注意力机制的seq2seq模型/架构/框架
  • 核心思想
    • 基于注意力机制
    • 自注意力
    • 一般注意力
  • 作用
    • 捕获超长距离语义关系
    • 并行计算
    • 灵活性: 处理不同的数据, 文本/语音/图像/视频
    • 扩展性: 层数和多头数量可调, transformer默认是6层, 8个头

2 transformer架构

1749118800244

  • 输入部分
    • 词嵌入层
    • 位置编码层
  • 输出部分
    • 线性层
    • softmax层
  • 编码器部分
    • 多头自注意力子层
    • 前馈全连接子层
    • 残差连接层
    • 规范化层(层归一化)
  • 解码器部分
    • 掩码多头自注意力子层
    • 编码器-解码器堵头一般注意力子层
    • 前馈全连接子层
    • 残差连接层
    • 规范化层(层归一化)

3 输入

3.1 文本嵌入层

  • 概念

    • 将token转换成词向量过程
    • nn.Embedding()
  • 代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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
    # 输入部分是由 词嵌入层和位置编码层组成   x = word_embedding + position_encoding
    import torch
    import torch.nn as nn
    import math


    # 词嵌入层
    class Embeddings(nn.Module):
    # todo:1- 定义构造方法 init
    def __init__(self, vocab_size, d_model):
    super().__init__()
    # 初始化属性
    self.vocab = vocab_size # 词表大小
    self.d_model = d_model # 词向量维度
    # 初始化词嵌入层对象
    # padding_idx: 将值为0的值, 不进行词向量, 用0填充
    self.embedding = nn.Embedding(num_embeddings=self.vocab,
    embedding_dim=self.d_model,
    padding_idx=0)

    # todo:2- 定义forward方法 前向传播
    def forward(self, x):
    # 词嵌入结果乘以根号维度数
    # 最终的词向量值和后续位置编码信息差不太多, 实现信息平衡
    # 后续注意力机制使用的缩放点积, 乘和除相抵消
    return self.embedding(x) * math.sqrt(self.d_model)


    if __name__ == '__main__':
    vocab_size = 1000
    d_model = 512
    # 创建测试数据
    x = torch.LongTensor([[100, 2, 421, 508], [491, 998, 1, 0]])
    # 创建词嵌入对象
    my_embedding = Embeddings(vocab_size, d_model)
    # 调用对象实现词嵌入
    embedded_result = my_embedding(x)
    print('embedded_result--->', embedded_result.shape, embedded_result)

3.2 位置编码器

  • 概念

    • 通过一些计算方式给词向量引入位置信息工具
    • 位置编码器替代rnn/lstm/gru中的顺序执行 -> 拿到token和token位置信息
  • 作用

    • transformer中不使用rnn/lstm/gru计算语义, 没有位置概念
    • 引入位置信息
    • x 我 x -> 没有位置信息:我x(前x) = 我x(后x) 引入位置信息:我x(前x) != 我x(后x)
  • 方法

    • 使用正弦和余弦函数

      1749263379065

    • 计算每个token在512维度上所有位置信息

    • PE -> 词维度上的位置信息

      • 索引下标偶数位用sin(第1,3,5…词), 索引下标奇数位用cos(第2,4,6…词)
      • 我 -> [sin(), cos(), sin(), …]
      • 爱 -> [sin(), cos(), …]
  • transformer中为什么要引入位置编码?

    • 注意机制计算时只算token和token之间的语义关系/相关性, 没有考虑位置关系, 需要引入位置编码
    • transformer中使用正弦和余弦函数计算位置信息值
      • 周期性 -> sin(α+β) = sin(α)cos(β) + sin(β)cos(α)
        • 我-> sin(10) -> sin(10+20) = sin(10)cos(20) + sin(20)cos(10) = 爱
        • 可以学习到我和爱之间的位置关系(规律)
      • 取值范围[-1,1], 避免值太大或太小, 导致梯度消失或爆炸
  • 代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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
    56
    57
    58
    59
    60
    61
    # 位置编码器
    class PositionalEncoding(nn.Module):
    # todo:1- init方法, 计算出n个词位置矩阵
    def __init__(self, d_model, max_len=5000, dropout_p=0.1):
    super().__init__()
    # 初始化属性
    self.d_model = d_model # 词向量维度, 模型维度
    self.max_len = max_len # 句子最大长度
    self.dropout = nn.Dropout(p=dropout_p)

    # 获取句子所有token索引下标,作为pos
    # .unsqueeze(1)->在1轴升维 [[0],[1],...]
    pos = torch.arange(0, self.max_len).unsqueeze(1)
    # print('pos--->', pos.shape, pos)
    # 创建一个pe全0矩阵, 存储位置信息 形状(句子长度, 词维度)
    pe = torch.zeros(size=(self.max_len, self.d_model))
    # print('pe--->', pe.shape, pe)
    # 获取2i结果, 对向量维度d_model取偶数下标值
    _2i = torch.arange(0, self.d_model, 2).float()
    # print('_2i--->', _2i)
    # 计算位置信息 奇数位的词sin 偶数位的词cos
    pe[:, ::2] = torch.sin(pos / 10000 ** (_2i / self.d_model))
    pe[:, 1::2] = torch.cos(pos / 10000 ** (_2i / self.d_model))
    # 将pe位置矩阵升维, 三维数据集
    pe = pe.unsqueeze(0)
    # print('pe--->', pe.shape, pe)
    # 存储到内存中, 后续便于加载
    # pe属性中存储的是pe矩阵结果
    self.register_buffer('pe', pe)
    # todo:2- forward方法 将位置信息添加到词嵌入结果中
    def forward(self, x):
    """
    :param x: 词嵌入层的输出结果
    :return: 编码器的输入x
    """
    print('x--->', x.shape, x)
    # x.shape[1], 句子中有多少个真实的token, 就在pe矩阵中取前多少个就可以
    print('x.shape[1]--->', x.shape[1])
    print('self.pe[:, :x.shape[1], :]--->', self.pe[:, :x.shape[1], :].shape, self.pe[:, :x.shape[1], :])
    return x + self.pe[:, :x.shape[1], :]


    if __name__ == '__main__':
    vocab_size = 1000
    d_model = 512
    # 创建测试数据
    # max_len=60, 句子最大长度
    # 当前第1个句子长度为4, 后续需要补56个0
    # 当前第2个句子长度为4
    x = torch.LongTensor([[100, 2, 421, 508], [491, 998, 1, 211]])
    # 创建词嵌入对象
    my_embedding = Embeddings(vocab_size, d_model)
    # 调用对象实现词嵌入
    embedded_result = my_embedding(x)
    print('embedded_result--->', embedded_result.shape, embedded_result)

    # 创建pe位置矩阵 生成位置特征数据[1,60,512]
    my_pe = PositionalEncoding(d_model=d_model, dropout_p=0.1, max_len=60)
    # 调用位置编码对象
    pe_result = my_pe(embedded_result)
    print('pe_result--->', pe_result.shape, pe_result)

4 编码器

4.1 掩码张量

  • 概念

    • 掩盖一些信息的二进制张量或下三角矩阵张量
  • 作用

    • 屏蔽填充的信息
    • 屏蔽未来的信息
  • transformer中使用

    • 编码器中只能使用padding mask
    • 解码器中使用padding mask 和 casual mask
  • 代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    import torch
    import matplotlib.pyplot as plt


    # tril():生成下三角矩阵
    # triu():生成上三角矩阵
    # diagonal: 移动对角线
    def subsequent_mask(size):
    # 下三角
    causal_mask = torch.tril(torch.ones(size=(size, size)), diagonal=0)
    # 上三角
    # causal_mask = torch.triu(torch.ones(size=(size, size)), diagonal=0)
    return causal_mask


    if __name__ == '__main__':
    causal_mask = subsequent_mask(20)
    print('causal_mask--->', causal_mask.shape, causal_mask)

    # 绘图
    plt.figure()
    plt.imshow(causal_mask)
    plt.show()

    # 模拟自回归,进行自回归掩码
    scores = torch.randn(size=(5, 5))
    mask = subsequent_mask(5)
    print('mask==0--->', mask==0)
    masked_result = scores.masked_fill(mask==0, value=float('-inf'))
    print('masked_result--->', masked_result)

4.2 自注意力机制

  • 概念

    • 在同一序列中进行注意力计算
    • q=k=v
  • 作用

    • 并行计算
    • 捕获更长距离的语义关系
  • 为什么除以​$\sqrt{d_k}$

    • 防止q*k^T乘积值过大, 产生梯度饱和, 导致梯度消失
  • 代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    from input import *


    # 定义缩放点积注意力规则函数, 方便后续调用
    def attention(query, key, value, mask=None, dropout=None):
    """
    注意力计算封装函数
    :param query: 输入x 解码器掩码多头自注意力子层输出
    :param key: 输入x 编码器输出结果
    :param value: 输入x 编码器输出结果
    :param mask: 是否掩码
    :param dropout: dropout层对象 函数名
    :return: 动态c, 权重概率矩阵
    """
    # todo:1- 获取d_k, 词维度数
    d_k = query.shape[-1]
    # print('d_k--->', d_k)
    # todo:2- q和k计算权重分数矩阵
    scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)
    # print('scores--->', scores.shape, scores)
    # todo:3- 判断是否需要进行掩码操作
    if mask is not None:
    scores = scores.masked_fill(mask == 0, value=-1e9)
    print('='*50)
    # print('scores--->', scores.shape, scores)
    # todo:4- 权重分数矩阵进行softmax操作, 得到权重概率矩阵
    p_attn = torch.softmax(scores, dim=-1)
    print('p_attn--->', p_attn.shape, p_attn)
    # todo:5- 判断是否对权重概率矩阵进行dropout正则化
    if dropout is not None:
    p_attn = dropout(p_attn)
    # todo:6- 计算动态c矩阵
    c = torch.matmul(p_attn, value)
    print('c--->', c.shape, c)
    return c, p_attn

    if __name__ == '__main__':
    vocab = 1000 # 词表大小是1000
    d_model = 512 # 词嵌入维度是512维

    # 输入x 形状是2 x 4
    x = torch.LongTensor([[100, 2, 421, 0], [491, 998, 0, 0]])

    # 输入部分的Embeddings类
    my_embeddings = Embeddings(vocab, d_model)
    embedded_result = my_embeddings(x)

    dropout_p = 0.1 # 置0概率为0.1
    max_len = 60 # 句子最大长度

    # 输入部分的PositionalEncoding类
    my_pe = PositionalEncoding(d_model, max_len, dropout_p)
    pe_result = my_pe(embedded_result)

    # 调用attention函数
    # 准备q,k,v
    query=key=value=pe_result # 自注意力
    print('query--->', query.shape)
    # 准备mask掩码张量 padding_mask
    # unsqueeze(1) -> 形状(2,1,4) 后续进行masked_fill操作, 会进行广播, 变成 (2,4,4)
    mask = (x!= 0).type(torch.uint8).unsqueeze(1)
    # casual mask
    # mask = torch.tril(torch.ones(4, 4))
    print('mask--->', mask.shape, mask)
    c, p_attn = attention(query, key, value, mask)
    print('c--->', c.shape, c)
    print('p_attn--->', p_attn.shape, p_attn)

4.3 多头注意力机制

  • 概念

    • 使用多个头并行计算注意力, 可以从不同子空间维度学习特征
  • 作用

    • 得到更丰富的特征
    • 增强模型表达能力
  • 多头指的是什么

    • 在进行注意力计算时由多个人分别取算不同子空间的注意力
  • 实现流程

    • q,k,v分别经过线性层计算, 得到wq,wk,wv
    • 将线性计算的q,k,v进行多头转换操作,分别计算注意力
    • 将多头注意力结果合并(类似于还原操作)后的结果经过线性层计算
  • 代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    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
    # 定义克隆函数, 用于克隆不同子层
    def clones(module, N):
    return nn.ModuleList(copy.deepcopy(module) for _ in range(N))


    # 创建多头注意力机制类
    class MultiHeadedAttention(nn.Module):
    # todo:1- init方法
    def __init__(self, head, d_model, dropout_p=0.1):
    super().__init__()
    assert d_model % head ==0, 'd_model不能被head整数'
    self.d_k = d_model // head
    self.dropout = nn.Dropout(p=dropout_p)
    self.head = head
    # 初始为None, 还没有计算注意力
    self.attn = None
    # 4个线性层
    # 前3个分别对q,k,v进行线性学习
    # 第1个对多头注意力拼接结果进行线性学习
    self.linears = clones(nn.Linear(d_model, d_model), 4)
    # print('self.linears--->', self.linears)
    # todo:2- forward方法
    def forward(self, query, key, value, mask=None):
    # todo:1- 获取batch_size大小
    batch_size = query.size()[0]
    # print('batch_size--->', batch_size)
    # todo:2- 准备空列表, 存储线性计算+变形结果
    output_list = []
    # todo:3- q,k,v分别进行线性计算
    for model, x in zip(self.linears, (query, key, value)):
    # print('model--->', model)
    # print('x--->', x)
    output = model(x)
    # todo:4- 线性计算结果变形 -> (batch_size, seq_len, head, d_k)
    # transpose(1, 2):词数和词向量相邻, 更好的学习特征
    output = output.view(batch_size, -1, self.head, self.d_k).transpose(1, 2)
    # todo:5- 将变形结果保存到空列表中
    output_list.append(output)
    # 获取q, k, v
    # print('output_list--->', len(output_list))
    query = output_list[0]
    key = output_list[1]
    value = output_list[2]
    # todo:6- 计算多头注意力, 调用attention函数 (batch_size, seq_len, head, d_k)
    x, p_attn = attention(query, key, value, mask)
    # print('x--->', x.shape)
    # todo:7- 多头注意力结果变形 -> (batch_size, seq_len, word_dim)
    x = x.transpose(1, 2).contiguous().view(batch_size, -1, self.head*self.d_k)
    # print('x--->', x.shape)
    # todo:8- 经过线性层计算返回输出结果
    # self.linears[-1]: 线性层对象
    x = self.linears[-1](x)
    return x


    if __name__ == '__main__':
    vocab = 1000 # 词表大小是1000
    d_model = 512 # 词嵌入维度是512维

    # 输入x 形状是2 x 4
    x = torch.LongTensor([[100, 2, 421, 0], [491, 998, 0, 0]])

    # 输入部分的Embeddings类
    my_embeddings = Embeddings(vocab, d_model)
    embedded_result = my_embeddings(x)

    dropout_p = 0.1 # 置0概率为0.1
    max_len = 60 # 句子最大长度

    # 输入部分的PositionalEncoding类
    my_pe = PositionalEncoding(d_model, max_len, dropout_p)
    pe_result = my_pe(embedded_result)

    # 调用attention函数
    # 准备q,k,v
    query=key=value=pe_result # 自注意力
    # print('query--->', query.shape)
    # 准备mask掩码张量 padding_mask
    # unsqueeze(1) -> 形状(2,1,4) 后续进行masked_fill操作, 会进行广播, 变成 (2,4,4)
    # 多头注意力机制,需要得到(2,1,1,4)形状mask
    mask = (x!= 0).type(torch.uint8).unsqueeze(1).unsqueeze(2)
    # casual mask
    # mask = torch.tril(torch.ones(4, 4))
    # print('mask--->', mask.shape, mask)
    # c, p_attn = attention(query, key, value, mask)
    # print('c--->', c.shape, c)
    # print('p_attn--->', p_attn.shape, p_attn)

    head=8
    # 创建多头注意力机制类对象
    my_mha = MultiHeadedAttention(head, d_model)
    # 调用多头注意力机制对象
    mha_result = my_mha(query, key, value, mask)
    print('mha_result--->', mha_result.shape, mha_result)

4.4 前馈全连接层

  • 概念

    • 由两层线性层和一层relu激活层
  • 作用

    • 提取更丰富的非线性特征, 增强模型表达能力
  • 代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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
    56
    57
    58
    59
    60
    61
    62
    63
    # 前馈全连接层
    class PositionwiseFeedForward(nn.Module):
    def __init__(self, d_model, d_ff, dropout_p=0.1):
    super().__init__()
    # 定义两层线性层
    # d_ff>d_model
    self.linear1 = nn.Linear(d_model, d_ff)
    self.linear2 = nn.Linear(d_ff, d_model)
    # 定义dropout层
    self.dropout = nn.Dropout(p=dropout_p)

    def forward(self, x):
    output = torch.relu(self.linear1(x))
    output = self.dropout(output)
    print('output--->', output.shape)
    return self.linear2(output)


    if __name__ == '__main__':
    vocab = 1000 # 词表大小是1000
    d_model = 512 # 词嵌入维度是512维

    # 输入x 形状是2 x 4
    x = torch.LongTensor([[100, 2, 421, 0], [491, 998, 0, 0]])

    # 输入部分的Embeddings类
    my_embeddings = Embeddings(vocab, d_model)
    embedded_result = my_embeddings(x)

    dropout_p = 0.1 # 置0概率为0.1
    max_len = 60 # 句子最大长度

    # 输入部分的PositionalEncoding类
    my_pe = PositionalEncoding(d_model, max_len, dropout_p)
    pe_result = my_pe(embedded_result)

    # 调用attention函数
    # 准备q,k,v
    query = key = value = pe_result # 自注意力
    # print('query--->', query.shape)
    # 准备mask掩码张量 padding_mask
    # unsqueeze(1) -> 形状(2,1,4) 后续进行masked_fill操作, 会进行广播, 变成 (2,4,4)
    # 多头注意力机制,需要得到(2,1,1,4)形状mask
    mask = (x != 0).type(torch.uint8).unsqueeze(1).unsqueeze(2)
    # casual mask
    # mask = torch.tril(torch.ones(4, 4))
    # print('mask--->', mask.shape, mask)
    # c, p_attn = attention(query, key, value, mask)
    # print('c--->', c.shape, c)
    # print('p_attn--->', p_attn.shape, p_attn)

    head = 8
    # 创建多头注意力机制类对象
    my_mha = MultiHeadedAttention(head, d_model)
    # 调用多头注意力机制对象
    mha_result = my_mha(query, key, value, mask)
    print('mha_result--->', mha_result.shape, mha_result)

    # 创建前馈全连接对象
    d_ff = 2048
    my_ff = PositionwiseFeedForward(d_model, d_ff)
    ff_result = my_ff(mha_result)
    print('ff_result--->', ff_result.shape)

seq2seq

seq2seq

1 RNN案例-seq2seq英译法

1.1 seq2seq模型介绍

  • 模型结构
    • 编码器 encoder
    • 解码器 decoder
    • 编码器和解码器中可以使用RNN模型或者是transformer模型
  • 工作流程
    • 编码器生成上下文语义张量 -> 什么是nlp? 将问题转换成语义张量
    • 解码器根据编码器的语义张量和上一时间步的预测值以及上一时间步的隐藏状态值进行当前时间步的预测
      • 自回归模式
  • 局限性
    • 信息瓶颈问题
    • 长序列问题

1.2 数据集介绍

1748849163083

  • 每行样本由英文句子和法文句子对组成, 中间用\t分隔开
  • 英文句子是编码器的输入序列, 法文句子是解码器的输出序列(预测序列)对应的真实序列

1.3 案例实现步骤

1.3.1 文本清洗工具函数

  • utils.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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
    # 用于正则表达式
    import re
    # 用于构建网络结构和函数的torch工具包
    import torch
    import torch.nn as nn
    from torch.utils.data import Dataset, DataLoader
    # torch中预定义的优化方法工具包
    import torch.optim as optim
    import time
    # 用于随机生成数据
    import random
    import numpy as np
    import matplotlib.pyplot as plt

    # 定义变量
    # 选择设备 cpu/gpu
    # 'cuda'->使用所有显卡 'cuda:0'->使用第一张显卡
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    # 起始符号下标
    # sos -> start of sentences
    SOS_token = 0
    # 结束符号下标
    EOS_token = 1
    # 文件路径
    data_path = 'data/eng-fra-v2.txt'
    # 最大句子长度, 预处理分析的结果
    MAX_LENGTH = 10


    # 定义处理文本的工具函数 处理句子中的特殊符号/大小写/换行符
    def normalizeString(s: str):
    # 转换成小写, 并删掉两端的空白符号
    str = s.lower().strip()
    # 正则表达式匹配标签符号'.?!' 转换成 ' .?!'
    str = re.sub(r'([.!?])', r' \1', str)
    # print('str--->', str)
    # 正则表达式匹配除a-z.!?之外的其他的符号 转换成 ' '
    str = re.sub(r'[^a-z.!?]+', r' ', str)
    # print('str--->', str)
    return str


    if __name__ == '__main__':
    str1 = 'I m sad.@'
    normalizeString(str1)

1.3.2 数据预处理

  • preprocess.py

    • 清洗文本和构建词表

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      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
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      from utils import *


      def my_getdata():
      # todo:1- 读取文件数据集, 得到 [[英文句子1, 法文句子1], [英文句子2, 法文句子2], ...]内存数据集
      # 1-1 with open 读取文件数据集
      with open(data_path, 'r', encoding='utf-8') as f:
      my_lines = f.read().strip().split('\n')
      # print('my_lines --->', my_lines)
      # 1-2 获取 [[英文句子1, 法文句子1], [英文句子2, 法文句子2], ...] 数据集格式
      # 定义两个空列表
      tmp_pair, my_pairs = [], []
      # 循环遍历my_lines
      for line in my_lines:
      # print('line--->', line) # i m . j ai ans .
      # 对my_lines中每行样本使用\t分割符进行分割后再循环遍历
      for item in line.split('\t'):
      # print('item--->', item)
      # 将每行样本中的英文句子和法文句子使用工具函数进行清洗, 保存到tmp_pair列表中
      tmp_pair.append(normalizeString(item))
      # 将tmp_pair列表保存到my_pairs列表中
      my_pairs.append(tmp_pair)
      # 重置tmp_pair列表
      tmp_pair = []
      # print('my_pairs的长度为--->', len(my_pairs))
      # print('my_pairs[:4]--->', my_pairs[:4])

      # todo:2-构建英文和法文词表 {词:下标} {下标:词}
      # 2-0: 初始化词表, 有SOS和EOS两个词
      english_word2index = {'SOS':0, 'EOS':1}
      # 定义第3个词起始下标
      english_word_n = 2
      french_word2index = {'SOS': 0, 'EOS': 1}
      french_word_n = 2

      # 2-1: 循环遍历my_pairs [['i m .', 'j ai ans .'], ...]
      for pair in my_pairs:
      # print('pair--->', pair) # ['i m .', 'j ai ans .']
      # 2-2: 对英文句子或法文句子根据 ' '空格进行分割, 再进行循环遍历
      for word in pair[0].split(' '):
      # print('word--->', word) # i m .
      # 2-3: 使用if语句, 判断当前词是否在词表中, 如果不在添加进去
      if word not in english_word2index.keys():
      english_word2index[word] = english_word_n
      # 更新词下标
      english_word_n+=1
      for word in pair[1].split(' '):
      # 2-3: 使用if语句, 判断当前词是否在词表中, 如果不在添加进去
      if word not in french_word2index.keys():
      french_word2index[word] = french_word_n
      # 更新词下标
      french_word_n+=1

      # 2-4 获取{下标:词}格式词表
      english_index2word = {v:k for k, v in english_word2index.items()}
      french_index2word = {v:k for k, v in french_word2index.items()}
      # print('english_word2index--->', len(english_word2index), english_word2index)
      # print('french_word2index--->', len(french_word2index), french_word2index)
      # print('english_index2word--->', len(english_index2word), english_index2word)
      # print('french_index2word--->', len(french_index2word), french_index2word)
      # print('english_word_n--->', english_word_n)
      # print('french_word_n--->', french_word_n)
      return english_word2index, english_index2word, english_word_n, french_word2index, french_index2word, french_word_n, my_pairs


      if __name__ == '__main__':
      (english_word2index, english_index2word, english_word_n,
      french_word2index, french_index2word, french_word_n, my_pairs) = my_getdata()
    • 构建数据源对象

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      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
      56
      57
      58
      59
      60
      61
      62
      63
      # 自定义张量数据源类
      class MyPairsDataset(Dataset):
      # todo:1- init构造方法, 初始化属性
      def __init__(self, my_pairs, english_word2index, french_word2index):
      self.my_pairs = my_pairs # [[], [], ...]
      self.english_word2index = english_word2index
      self.french_index2word = french_word2index
      # 获取数据集长度
      self.sample_len = len(my_pairs)

      # todo:2- len方法, 返回数据集的长度
      def __len__(self):
      return self.sample_len

      # todo:3- getitem方法, 对数据进行处理, 转换成张量数据对象
      def __getitem__(self, index):
      """
      转换成张量数据对象
      :param index: 数据集的下标 -> 第index个样本
      :return: tensor_x, tensor_y
      """
      # 3-1: 修正index, 防止超过下标边界
      index = min(max(index, 0), self.sample_len - 1)
      # print('index--->', index)
      # 3-2: 获取当前index样本中的 x和y
      x = self.my_pairs[index][0]
      y = self.my_pairs[index][1]
      # print('x--->', x)
      # print('y--->', y)
      # 3-3: 将x和y的字符串数据转换成下标表示 词表
      # self.english_word2index[word]: 根据key获取字典中的value
      x = [self.english_word2index[word] for word in x.split(' ')]
      y = [self.french_index2word[word] for word in y.split(' ')]
      # print('x--->', x)
      # print('y--->', y)
      # 3-4: 每个样本最后加EOS下标 结束符号
      x.append(EOS_token)
      y.append(EOS_token)
      # print('x--->', x)
      # print('y--->', y)
      # 3-5: 将下标列表转换成张量对象
      # device: 将张量创建到对应的设备上 GPU/CPU
      tensor_x = torch.tensor(x, dtype=torch.long, device=device)
      tensor_y = torch.tensor(y, dtype=torch.long, device=device)
      # print('tensor_x--->', tensor_x)
      # print('tensor_y--->', tensor_y)
      return tensor_x, tensor_y


      if __name__ == '__main__':
      (english_word2index, english_index2word, english_word_n,
      french_word2index, french_index2word, french_word_n, my_pairs) = my_getdata()
      # 创建自定义数据源对象
      my_dataset = MyPairsDataset(my_pairs, english_word2index, french_word2index)
      print('my_dataset数据集条目数--->', len(my_dataset))
      print(my_dataset[0])
      # 创建数据加载器对象
      my_dataloader = DataLoader(dataset=my_dataset, batch_size=1, shuffle=True)
      # 循环遍历数据加载器
      for i, (x, y) in enumerate(my_dataloader):
      print('x--->', x.shape, x)
      print('y--->', y.shape, y)
      break

1.3.3 构建基于GRU的编码器和解码器

  • 构建基于GRU的编码器 encoderrnn.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    from preprocess import *
    class EncoderRNN(nn.Module):
    # todo:1- 定义构造方法 init
    def __init__(self, input_size, hidden_size):
    super().__init__()
    # 输入特征维度属性 input_size是英文词表的大小
    self.input_size = input_size
    # 词嵌入层和隐藏层特征维度属性 共用
    self.hidden_size = hidden_size
    # 词嵌入层对象属性
    self.embedding = nn.Embedding(num_embeddings=self.input_size,
    embedding_dim=self.hidden_size)
    # gru层对象属性
    # input_size: 上一层输出特征维度数
    # hidden_size: 当前层输出特征维度数
    # batch_first: x和hidden形状 -> (句子数, 句子长度, 词维度)
    self.gru = nn.GRU(input_size=self.hidden_size, hidden_size=self.hidden_size, batch_first=True)
    # todo:2- 定义前向传播方法 forward
    def forward(self, input, hidden):
    # print('input--->', input.shape)
    # 词嵌入操作 词向量化
    embedded = self.embedding(input)
    # print('embedded--->', embedded.shape)
    # gru层前向传播操作
    output, hn = self.gru(embedded, hidden)
    # print('output--->', output.shape)
    # print('hn--->', hn.shape)
    return output, hn
    # todo:3- 定义初始化隐藏状态值方法 inithidden
    def inithidden(self):
    return torch.zeros(size=(1, 1, self.hidden_size), device=device)


    if __name__ == '__main__':
    # 获取数据
    (english_word2index, english_index2word, english_word_n,
    french_word2index, french_index2word, french_word_n, my_pairs) = my_getdata()
    # 创建张量数据集
    my_dataset = MyPairsDataset(my_pairs, english_word2index, french_word2index)
    # 创建数据加载器
    # batch_size: 当前设置为1, 因为句子长度不一致
    my_dataloader = DataLoader(dataset=my_dataset, batch_size=1, shuffle=True)
    # 创建编码器对象
    my_encoderrnn = EncoderRNN(input_size=english_word_n, hidden_size=256).to(device=device)
    for i, (x, y) in enumerate(my_dataloader):
    # 一次性喂数据
    # 初始化隐藏状态值
    hidden = my_encoderrnn.inithidden()
    encoder_output, hn = my_encoderrnn(x, hidden)
    print('encoder_output--->', encoder_output.shape)
    print('hn--->', hn.shape)

    # 一个时间步一个时间步喂数据, gru底层实现 了解,解码器需要这样操作
    hidden = my_encoderrnn.inithidden()
    # x.shape[1]: 获取当前x的token数, 时间步数
    for j in range(x.shape[1]):
    # print('x--->', x)
    # print('x[0]--->', x[0])
    # print('x[0][j]--->', x[0][j])
    tmp_x = x[0][j].view(1, -1)
    print('tmp_x--->', tmp_x)
    output, hidden = my_encoderrnn(tmp_x, hidden)
    print('观察:最后一个时间步output输出是否相等') # hidden_size = 8 效果比较好
    print('encoder_output[0][-1]===>', encoder_output[0][-1])
    print('output===>', output)
    break
  • 构建基于GRU的解码器 decoderrnn.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    from encoderrnn import *

    class DecoderRNN(nn.Module):
    # todo:1- 定义构造方法 init
    def __init__(self, output_size, hidden_size):
    super().__init__()
    # 初始化法文词表大小维度属性=线性输出层的维度
    self.output_size = output_size
    # 初始化gru隐藏层和词嵌入层的维度属性 共用
    self.hidden_size = hidden_size
    # 初始化词嵌入层
    # num_embeddings: 法文词表大小
    # embedding_dim: 词向量初始维度
    self.embeding = nn.Embedding(num_embeddings=self.output_size, embedding_dim=self.hidden_size)
    # 初始化gru层
    self.gru = nn.GRU(input_size=self.hidden_size, hidden_size=self.hidden_size, batch_first=True)

    # 初始化全连接层 线性层+激活层
    # out_features: 法文词表大小 预测出n个词的生成概率
    self.out = nn.Linear(in_features=self.hidden_size, out_features=self.output_size)
    # dim:一定是-1, 按行处理
    self.softmax = nn.LogSoftmax(dim=-1)

    # todo:2- 定义前向传播方法 forward
    def forward(self, input, hidden):
    print('input--->', input.shape)
    # 词嵌入操作
    embedded = self.embeding(input)
    print('embedded--->', embedded.shape)
    # 通过relu激活函数引入非线性因素, 防止过拟合(x<0置为0, 神经元死亡)
    embedded = torch.relu(embedded)
    print('embedded--->', embedded.shape)
    # gru层操作
    # ouput: 输入input的语义信息, 形状为(句子数, 句子长度, 词维度) 三维
    output, hidden = self.gru(embedded, hidden)
    print('output--->', output.shape, output)
    # 全连接层操作
    # output[0]: 全连接层一般是二维数据, 所以要取出当前token的二维表示
    # 返回的output是 logsoftmax结果, 后续的值可能会有负值, 不是softmax的概率值
    output = self.softmax(self.out(output[0]))
    print('output--->', output.shape, output)
    return output, hidden


    if __name__ == '__main__':
    # 获取数据
    (english_word2index, english_index2word, english_word_n,
    french_word2index, french_index2word, french_word_n, my_pairs) = my_getdata()
    # 创建张量数据集
    my_dataset = MyPairsDataset(my_pairs, english_word2index, french_word2index)
    # 创建数据加载器
    # batch_size: 当前设置为1, 因为句子长度不一致
    my_dataloader = DataLoader(dataset=my_dataset, batch_size=1, shuffle=True)
    # 创建编码器对象
    my_encoderrnn = EncoderRNN(input_size=english_word_n, hidden_size=256).to(device=device)
    # 创建解码器对象
    output_size = french_word_n
    hidden_size = 256
    my_decoderrnn = DecoderRNN(output_size, hidden_size).to(device)
    for i, (x, y) in enumerate(my_dataloader):
    # 编码器进行编码 一次性喂数据
    # 初始化隐藏状态值
    hidden = my_encoderrnn.inithidden()
    encoder_output, hn = my_encoderrnn(x, hidden)
    print('encoder_output--->', encoder_output.shape)
    print('hn--->', hn.shape, hn)

    # 解码器进行解码, 自回归, 一个一个token进行解码
    for j in range(y.shape[1]) :
    # 获取当前预测token时间步的输入x(等同于上一时间步的预测y)
    # 当前以真实y中的每个token作为输入, 模拟解码器的界面过程, 实际上第一个输入token一定是起始符号
    tmp_y = y[0][j].view(1, -1)
    # 进行解码
    # 初始的隐藏状态值=编码器最后一个时间步的隐藏状态值
    my_decoderrnn(tmp_y, hn)
    break
    break
  • 构建基于GRU和Attention的解码器 decoderrnn.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    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
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    # 带加性注意力机制的解码器
    class AttnDecoderRNN(nn.Module):
    # todo:1- 定义构造方法 init
    def __init__(self, output_size, hidden_size, dropout_p=0.2, max_length=MAX_LENGTH):
    super().__init__()
    # 初始化词嵌入层的输入维度和全连接层的输出维度一致
    self.output_size = output_size
    # 初始化编码器解码器隐藏层维度属性 解码器的第一个隐藏状态值=编码器的最后一个隐藏状态值
    # 初始化词嵌入层维度属性 共享
    self.hidden_size = hidden_size
    # 初始化最大句子长度属性 -> 所有句子 c的长度固定
    self.max_length = max_length
    # 初始化dropout概率属性
    self.dropout_p = dropout_p
    # 初始化 embeding层
    self.embedding = nn.Embedding(num_embeddings=self.output_size, embedding_dim=self.hidden_size)
    # 初始化 gru层
    self.gru = nn.GRU(input_size=self.hidden_size, hidden_size=self.hidden_size, batch_first=True)
    # 初始化 全连接层
    self.out = nn.Linear(in_features=self.hidden_size, out_features=self.output_size)
    self.softmax = nn.LogSoftmax(dim=-1)

    # 初始化注意力机制中两个线性层
    """
    q:解码器当前预测时间步的隐藏状态值
    k:解码器当前预测时间步的上一时间步隐藏状态值
    v:编码器的output输出
    q,k,v三个特征维度相同 都是hidden_size
    """
    # in_features: q和k的特征维度拼接
    # out_features: 后续权重概率矩阵->(1, 1, max_len) 和 V矩阵相乘 V->(1, max_len, hidden_size)
    self.attn = nn.Linear(in_features=self.hidden_size + self.hidden_size, out_features=self.max_length)
    # in_features: q和c的特征维度拼接
    # out_features: 输出的维度和gru层的输入维度保持一致
    self.attn_combine = nn.Linear(in_features=self.hidden_size + self.hidden_size, out_features=self.hidden_size)
    # 初始化dropout层
    self.dropout = nn.Dropout(p=self.dropout_p)

    # todo:2- 定义前向传播方法 forward
    def forward(self, input, hidden, encoder_outputs):
    """
    前向传播计算
    :param input: q, 解码器当前预测时间步的输入x, 也是上一个时间步预测的输出y
    :param hidden: k, 上一个时间步的隐藏状态值, 第一个时间步的上一个隐藏状态值=编码器最后一个时间步的隐藏状态值
    :param encoder_outputs: v, 编码器的输出 output, 后续是统一长度都为10, 10个token, 不足10个token用0填充
    :return: 预测词表概率向量, 当前时间步的隐藏状态值, 权重概率矩阵
    """
    # 2-1 词嵌入操作
    embedded = self.embedding(input)
    # 使用dropout防止过拟合
    embedded = self.dropout(embedded)
    print('embedded--->', embedded.shape, embedded)

    # 2-2 计算权重分数矩阵, 之后再计算权重概率矩阵
    # q和k在特征维度轴拼接 + 线性计算 + softmax计算
    # embedded[0]: 获取二维向量表示, 线性层一般接收二维数据
    attn_weights = torch.softmax(self.attn(torch.cat(tensors=[embedded[0], hidden[0]], dim=1)), dim=-1)
    print('attn_weights--->', attn_weights.shape, attn_weights)
    # print(torch.sum(input=attn_weights))

    # 2-3 计算动态c, 加权求和 权重概率矩阵和v进行三维矩阵乘法
    # bmm() 三维矩阵乘法, 目前attn_weights和encoder_outputs二维矩阵
    attn_applied = torch.bmm(attn_weights.unsqueeze(0), encoder_outputs.unsqueeze(0))
    print('attn_applied--->', attn_applied.shape, attn_applied)

    # 2-4 q和动态c融合线性计算, 得到gru的输入x
    # unsqueeze():得到三维数据, gru的输入x的形状要求
    output = self.attn_combine(torch.cat(tensors=[embedded[0], attn_applied[0]], dim=1)).unsqueeze(0)
    print('output--->', output.shape, output)
    # relu激活函数, 非线性因素
    output = torch.relu(output)

    # 2-5 gru层操作
    output, hidden = self.gru(output, hidden)
    print('output--->', output.shape, output)
    print('hidden--->', hidden.shape, hidden)

    # 2-6 全连接层操作
    output = self.softmax(self.out(output[0]))
    print('output--->', output.shape, output)
    return output, hidden, attn_weights


    if __name__ == '__main__':
    # 获取数据
    (english_word2index, english_index2word, english_word_n,
    french_word2index, french_index2word, french_word_n, my_pairs) = my_getdata()
    # 创建张量数据集
    my_dataset = MyPairsDataset(my_pairs, english_word2index, french_word2index)
    # 创建数据加载器
    # batch_size: 当前设置为1, 因为句子长度不一致
    my_dataloader = DataLoader(dataset=my_dataset, batch_size=1, shuffle=True)
    # 创建编码器对象
    my_encoderrnn = EncoderRNN(input_size=english_word_n, hidden_size=256).to(device=device)
    # 创建解码器对象
    output_size = french_word_n
    hidden_size = 256
    # my_decoderrnn = DecoderRNN(output_size, hidden_size).to(device)

    # 创建带attn的解码器对象
    my_attndecoderrnn = AttnDecoderRNN(output_size, hidden_size).to(device)
    for i, (x, y) in enumerate(my_dataloader):
    # print('x--->', x.shape)
    # 编码器进行编码 一次性喂数据
    # 初始化隐藏状态值
    hidden = my_encoderrnn.inithidden()
    encoder_output, hn = my_encoderrnn(x, hidden)
    print('encoder_output--->', encoder_output.shape, encoder_output)
    # print('hn--->', hn.shape, hn)

    # 获取填充成最大程度的编码器c或者output
    # 初始化全0的张量 形状(10, 256) [[0,0,0,0,0,0,...],[],[]]
    encoder_output_c = torch.zeros(size=(MAX_LENGTH, my_encoderrnn.hidden_size), device=device)
    # 将encoder_output真实值赋值到encoder_output_c对应位置
    for idx in range(x.shape[1]):
    encoder_output_c[idx] = encoder_output[0][idx]
    print('encoder_output_c--->', encoder_output_c.shape, encoder_output_c)
    # 解码器进行解码, 自回归, 一个一个token进行解码
    for j in range(y.shape[1]):
    # 获取当前预测token时间步的输入x(等同于上一时间步的预测y)
    # 当前以真实y中的每个token作为输入, 模拟解码器的界面过程, 实际上第一个输入token一定是起始符号
    tmp_y = y[0][j].view(1, -1)
    # 进行解码
    # 初始的隐藏状态值=编码器最后一个时间步的隐藏状态值
    # my_decoderrnn(tmp_y, hn)
    # hn:编码器端最后一个时间步的隐藏状态值, 也是解码器端第一个时间步的初始的隐藏状态值
    print('hn--->', hn.shape, hn)
    output, hidden, attn_weights = my_attndecoderrnn(tmp_y, hn, encoder_output_c)
    print('=' * 80)
    print('output--->', output.shape, output)
    print('hidden--->', hidden.shape, hidden)
    print('attn_weights--->', attn_weights.shape, attn_weights)
    break
    break

1.3.4 构建模型训练函数并进行训练

  • Teacher Forcing介绍

    • 概念
      • 解码时用真实y值作为输入
      • 一种增强模型训练效果的技术
    • 作用
      • 加快模型收敛速度
      • 稳定模型训练过程
      • 使用真实y,损失值小
    • 优点
      • 加快模型收敛速度
      • 稳定模型训练过程
      • 使用真实y,损失值小
    • 缺点
      • 训练和测试时不一致, 测试推理没有真实y, 导致模型过拟合
    • 改进方法
      • Scheduled Sampling: 计划采样
        • 随机生成一个随机数和Teacher Forcing比例进行比较
        • if判断
          • 小于等于比例, 真实y
          • 大于比例, 预测y
      • Curriculum Learning: 课程学习
        • Teacher Forcing比例: 前期大, 后期小, 衰减
        • 调整训练样本的顺序
          • 先训练短句, 再训练长度
          • 先训练高质量句子, 再训练低质量句子
  • 构建内部迭代训练函数 train.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    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
    from decoderrnn import *

    # 模型训练参数
    mylr = 1e-4
    epochs = 2
    # 设置teacher_forcing比率为0.5
    teacher_forcing_ratio = 0.5
    # 1000次迭代打印一次信息
    print_interval_num = 1000
    # 100次迭代绘制损失曲线
    plot_interval_num = 100

    def train_iters(x,y,
    my_encoderrnn:EncoderRNN,
    my_attndecoderrnn:AttnDecoderRNN,
    myadam_encode: optim.Adam,
    myadam_decode: optim.Adam,
    mynllloss: nn.NLLLoss):
    """
    模型训练的内部函数 -> 内循环代码封装
    :param x: 英文句子
    :param y: 真实法文句子
    :param my_encoderrnn: 编码器
    :param my_attndecoderrnn: 解码器
    :param myadam_encode: 编码器优化器
    :param myadam_decode: 解码器优化器
    :param mynllloss: 解码器损失函数对象
    :return: 当前句子的平均损失
    """
    # todo:1- 切换模型训练模式
    my_encoderrnn.train()
    my_attndecoderrnn.train()
    # todo:2- 初始化编码器隐藏状态值
    encode_h0 = my_encoderrnn.inithidden()
    # todo:3- 调用编码器获取v和k output就是v k就是解码器的初始隐藏状态值
    encode_output, encode_hn = my_encoderrnn(x, encode_h0)
    # print('encode_output--->', encode_output.shape, encode_output)
    # print('encode_hn--->', encode_hn.shape, encode_hn)
    # todo:4- 处理v, 统一长度, 都是10 v
    encode_output_c = torch.zeros(size=(MAX_LENGTH, my_encoderrnn.hidden_size), device=device)
    # print('encode_output_c--->', encode_output_c.shape, encode_output_c)
    for idx in range(x.shape[1]):
    encode_output_c[idx] = encode_output[0, idx]
    # print('encode_output_c--->', encode_output_c.shape, encode_output_c)
    # todo:5- 准备解码器第一个时间步的参数 q,k,v
    # 准备k
    decode_hidden = encode_hn
    # 准备q
    input_y = torch.tensor(data=[[SOS_token]], device=device)
    # print('input_y--->', input_y.shape, input_y)
    # todo:6- 初始化变量, 存储信息
    myloss = 0.0 # 当前句子的总损失
    iters_num = 0 # 当前句子的token数
    # todo:7- 判断教师强制机制是否成立, 返回True或False
    user_teacher_forcing = True if random.random() < teacher_forcing_ratio else False
    # print('user_teacher_forcing--->', user_teacher_forcing)
    # todo:8- 解码器自回归解码
    # 预测什么时候结束? ①到达循环次数,法文句子长度 ②预测出EOS_token
    for idx in range(y.shape[1]):
    # 调用解码器模型对象, 返回预测token, 隐藏状态值, 注意力机制概率矩阵
    output_y, hidden, attn_weights = my_attndecoderrnn(input_y, decode_hidden, encode_output_c)
    # print('output_y--->', output_y.shape, output_y)
    # 获取当前时间步真实的token
    target_y = y[0][idx].view(1)
    # print('target_y--->', target_y.shape, target_y)
    # 计算损失值
    myloss += mynllloss(output_y, target_y)
    # print('myloss--->', myloss)
    # 更新iters_num数
    iters_num += 1
    # print('iters_num--->', iters_num)
    # 使用教师强制机制, 判断下一时间步使用真实token还是预测token
    if user_teacher_forcing:
    # input_y = y[0][idx].view(1, -1)
    input_y = target_y.view(1, -1)
    # print('input_y--->', input_y.shape, input_y)
    else:
    # 返回最大值和对应的下标
    topv, topi = output_y.topk(1)
    # print('topv--->', topv)
    # print('topi--->', topi)
    # 预测出结束符号, 解码结束
    if topi.item() == EOS_token:
    break
    input_y = topi

    # todo:9-梯度清零, 反向传播, 梯度更新
    myadam_encode.zero_grad()
    myadam_decode.zero_grad()

    myloss.backward()

    myadam_encode.step()
    myadam_decode.step()

    # todo:10- 句子的平均损失 总损失/token数
    return myloss.item() / iters_num
  • 构建模型训练函数 train.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    def train_seq2seq():
    # 获取数据
    (english_word2index, english_index2word, english_word_n,
    french_word2index, french_index2word, french_word_n, my_pairs) = my_getdata()
    # 实例化 mypairsdataset对象 实例化 mydataloader
    mypairsdataset = MyPairsDataset(my_pairs, english_word2index, french_word2index)
    mydataloader = DataLoader(dataset=mypairsdataset, batch_size=1, shuffle=True)

    # 实例化编码器 my_encoderrnn 实例化解码器 my_attndecoderrnn
    my_encoderrnn = EncoderRNN(english_word_n, 256).to(device)
    my_attndecoderrnn = AttnDecoderRNN(output_size=french_word_n, hidden_size=256, dropout_p=0.1, max_length=10).to(
    device)

    # 实例化编码器优化器 myadam_encode 实例化解码器优化器 myadam_decode
    myadam_encode = optim.Adam(my_encoderrnn.parameters(), lr=mylr)
    myadam_decode = optim.Adam(my_attndecoderrnn.parameters(), lr=mylr)

    # 实例化损失函数 mycrossentropyloss = nn.NLLLoss()
    mynllloss = nn.NLLLoss()

    # 定义模型训练的参数
    plot_loss_list = []

    # 循环轮次 epoch
    for epoch_idx in range(1, epochs + 1):
    # 初始化打印日志的总损失 和 绘图总损失
    print_loss_total = 0.0
    plot_loss_total = 0.0
    # 开始时间
    starttime = time.time()
    # 循环迭代次数, batch数
    # start: 默认为0, 第一条数据的标号为0; 1->第一条数据的标号为1
    for item, (x, y) in enumerate(mydataloader, start=1):
    # 模型训练, 调用内部迭代函数
    loss = train_iters(x, y,
    my_encoderrnn,
    my_attndecoderrnn,
    myadam_encode,
    myadam_decode,
    mynllloss)
    # print('loss--->', loss)
    # 统计损失值
    print_loss_total += loss
    plot_loss_total += loss
    # 1000次迭代打印一次日志
    if item % 1000 == 0:
    print_loss_avg = print_loss_total / print_interval_num
    # 重置print_loss_total 0
    print_loss_total = 0.0
    # 打印日志,日志内容分别是:训练耗时,当前迭代步,当前进度百分比,当前平均损失
    print('轮次%d 损失%.6f 时间:%d' % (epoch_idx, print_loss_avg, time.time() - starttime))
    # 100次收集一次损失, 用于绘图
    if item % 100 == 0:
    plot_loss_avg = plot_loss_total / plot_interval_num
    plot_loss_list.append(plot_loss_avg)
    plot_loss_total = 0.0
    torch.save(my_encoderrnn.state_dict(), './model/my_encoderrnn_model_%d.bin' % (epoch_idx))
    torch.save(my_attndecoderrnn.state_dict(), './model/my_attndecoderrnn_model_%d.bin' % (epoch_idx))

    # 绘制损失值的曲线图
    plt.figure()
    plt.plot(plot_loss_list.detach().numpy())
    plt.savefig('./image/plot_loss_list.png')
    plt.show()
    return plot_loss_list


    if __name__ == '__main__':
    plot_loss_list = train_seq2seq()

1.3.5 构建模型评估函数并测试

  • 构建模型评估函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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
    56
    57
    58
    from decoderrnn import *

    PATH1 = 'model/my_encoderrnn_2.pth'
    PATH2 = 'model/my_attndecoderrnn_2.pth'

    def seq2seq_evaluate(x,
    my_encoderrnn: EncoderRNN,
    my_attndecoderrnn: AttnDecoderRNN,
    french_index2word):
    """
    推理内部函数, 得到预测的法文
    :param x: 需要推理的英文句子
    :param my_encoderrnn: 编码器
    :param my_attndecoderrnn: 解码器
    :param french_index2word: 法文词汇表, 根据最大概率的下标从词表中获取法文词
    :return: 法文列表, 注意力权重概率矩阵
    """
    with torch.no_grad():
    my_encoderrnn.eval()
    my_attndecoderrnn.eval()
    # todo: 1- 编码器编码
    encode_h0 = my_encoderrnn.inithidden()
    encode_output, encode_hn = my_encoderrnn(x, encode_h0)
    # todo: 2- 处理编码的输出 得到解码器的参数v
    encode_output_c = torch.zeros(size=(MAX_LENGTH, my_encoderrnn.hidden_size), device=device)
    for idx in range(x.shape[1]):
    encode_output_c[idx] = encode_output[0, idx]
    # todo: 3- 准备解码器的q和k参数
    decode_hidden = encode_hn
    input_y = torch.tensor(data=[[SOS_token]], device=device)
    # todo: 4- 定义变量 预测词空列表
    decode_words = []
    # todo: 6- 创建(10,10)全0张量, 存储每个时间步的注意力权重
    # (10, 10) -> 10:最多10个时间步 10:权重概率矩阵特征数为10
    decoder_attentions = torch.zeros(size=(MAX_LENGTH, MAX_LENGTH), device=device)
    # todo: 7- 解码器解码
    for i in range(MAX_LENGTH):
    # 解码
    output_y, decode_hidden, attn_weights = my_attndecoderrnn(input_y, decode_hidden, encode_output_c)
    # print('attn_weights--->', attn_weights.shape, attn_weights)
    # 保存当前时间步的attn_weights
    decoder_attentions[i] = attn_weights
    # print('decoder_attentions--->', decoder_attentions.shape, decoder_attentions)
    # 获取当前时间步的预测结果 topv topi
    # topi = torch.argmax(output_y)
    topv, topi = output_y.topk(1)
    # 判断topi是否是EOS_token下标值
    # 如果是,解码结束
    if topi.item() == EOS_token:
    decode_words.append('<EOS>')
    break
    else:
    decode_words.append(french_index2word[topi.item()])
    # 进行下一个时间步的预测
    input_y = topi
    # 返回法文列表, 注意力权重概率矩阵
    # [: i+1]->后续的值都为0,没有意义
    return decode_words, decoder_attentions[: i+1]
  • 调用模型评估函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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
    # 定义模型推理函数
    def inference():
    # todo:1- 加载推理数据集
    (english_word2index, english_index2word, english_word_n,
    french_word2index, french_index2word, french_word_n, my_pairs) = my_getdata()
    # todo:2- 创建张量数据集
    my_dataset = MyPairsDataset(my_pairs, english_word2index, french_word2index)
    # todo:3- 加载模型
    # 编码器模型对象
    my_encoderrnn = EncoderRNN(input_size=english_word_n, hidden_size=256)
    print('my_encoderrnn--->', my_encoderrnn)
    # 加载模型参数
    # map_location: 将模型加载到什么设备中
    # lambda storage,loc: storage:保存时在哪个设备,加载就在哪个设备
    # strict: 是否严格按照创建时键值匹配加载 -> init方法中gru层属性名
    # True: 匹配不成功, 报错 False: 不报错, 但是不执行不匹配的层
    my_encoderrnn.load_state_dict(torch.load(PATH1, map_location=lambda storage,loc: storage), strict=False)
    print('my_encoderrnn--->', my_encoderrnn)
    # 解码器模型对象
    my_attndecoderrnn = AttnDecoderRNN(output_size=french_word_n, hidden_size=256)
    my_attndecoderrnn.load_state_dict(torch.load(PATH2, map_location=lambda storage,loc: storage), strict=False)
    # todo:4- 准备3条测试样本
    my_samplepairs = [['i m impressed with your french .', 'je suis impressionne par votre francais .'],
    ['i m more than a friend .', 'je suis plus qu une amie .'],
    ['she is beautiful like her mother .', 'elle est belle comme sa mere .']]
    print('my_samplepairs--->', len(my_samplepairs))
    # todo:5- 对测试样本进行处理, 训练时怎么做特征工程,推理时一样
    for idx, pair in enumerate(my_samplepairs):
    x = pair[0]
    y = pair[1]
    # print('x--->', x)
    # print('y--->', y)
    # 对x转换成下标张量对象
    tem_x = [english_word2index[word] for word in x.split(' ')]
    tem_x.append(EOS_token)
    tensor_x = torch.tensor([tem_x], dtype=torch.long, device=device)
    # print('tensor_x--->', tensor_x.shape, tensor_x)
    # todo:6- 调用内部封装推理函数,进行推理
    decode_words, decoder_attentions = seq2seq_evaluate(tensor_x, my_encoderrnn, my_attndecoderrnn, french_index2word)
    # print('decode_words--->', decode_words)
    # print('decoder_attentions--->', decoder_attentions.shape, decoder_attentions)
    # todo:7- 将预测的法文列表转换成字符串文本
    output_sentence = ' '.join(decode_words)

    print('\n')
    print('需要推理的英文句子--->', x)
    print('真实的法文句子--->', y)
    print('推理的法文句子--->', output_sentence)


    if __name__ == '__main__':
    inference()
  • attention张量制图

2 transformer介绍

  • 概念
    • transformer是基于自注意力机制的seq2seq模型/架构/框架
  • 核心思想
    • 基于注意力机制
    • 自注意力
    • 一般注意力
  • 作用
    • 捕获超长距离语义关系
    • 并行计算
    • 灵活性: 处理不同的数据, 文本/语音/图像/视频
    • 扩展性: 层数和多头数量可调, transformer默认是6层, 8个头

3 transformer架构

1749118800244

  • 输入部分
    • 词嵌入层
    • 位置编码层
  • 输出部分
    • 线性层
    • softmax层
  • 编码器部分
    • 多头自注意力子层
    • 前馈全连接子层
    • 残差连接层
    • 规范化层(层归一化)
  • 解码器部分
    • 掩码多头自注意力子层
    • 编码器-解码器堵头一般注意力子层
    • 前馈全连接子层
    • 残差连接层
    • 规范化层(层归一化)

RNN案例-seq2seq英译法

RNN案例-seq2seq英译法

1 RNN案例-seq2seq英译法

1.1 seq2seq模型介绍

  • 模型结构
    • 编码器 encoder
    • 解码器 decoder
    • 编码器和解码器中可以使用RNN模型或者是transformer模型
  • 工作流程
    • 编码器生成上下文语义张量 -> 什么是nlp? 将问题转换成语义张量
    • 解码器根据编码器的语义张量和上一时间步的预测值以及上一时间步的隐藏状态值进行当前时间步的预测
      • 自回归模式
  • 局限性
    • 信息瓶颈问题
    • 长序列问题

1.2 数据集介绍

1748849163083

  • 每行样本由英文句子和法文句子对组成, 中间用\t分隔开
  • 英文句子是编码器的输入序列, 法文句子是解码器的输出序列(预测序列)对应的真实序列

1.3 案例实现步骤

1.3.1 文本清洗工具函数

  • utils.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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
    # 用于正则表达式
    import re
    # 用于构建网络结构和函数的torch工具包
    import torch
    import torch.nn as nn
    from torch.utils.data import Dataset, DataLoader
    # torch中预定义的优化方法工具包
    import torch.optim as optim
    import time
    # 用于随机生成数据
    import random
    import numpy as np
    import matplotlib.pyplot as plt

    # 定义变量
    # 选择设备 cpu/gpu
    # 'cuda'->使用所有显卡 'cuda:0'->使用第一张显卡
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    # 起始符号下标
    # sos -> start of sentences
    SOS_token = 0
    # 结束符号下标
    EOS_token = 1
    # 文件路径
    data_path = 'data/eng-fra-v2.txt'
    # 最大句子长度, 预处理分析的结果
    MAX_LENGTH = 10


    # 定义处理文本的工具函数 处理句子中的特殊符号/大小写/换行符
    def normalizeString(s: str):
    # 转换成小写, 并删掉两端的空白符号
    str = s.lower().strip()
    # 正则表达式匹配标签符号'.?!' 转换成 ' .?!'
    str = re.sub(r'([.!?])', r' \1', str)
    # print('str--->', str)
    # 正则表达式匹配除a-z.!?之外的其他的符号 转换成 ' '
    str = re.sub(r'[^a-z.!?]+', r' ', str)
    # print('str--->', str)
    return str


    if __name__ == '__main__':
    str1 = 'I m sad.@'
    normalizeString(str1)

1.3.2 数据预处理

  • preprocess.py

    • 清洗文本和构建词表

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      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
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      from utils import *


      def my_getdata():
      # todo:1- 读取文件数据集, 得到 [[英文句子1, 法文句子1], [英文句子2, 法文句子2], ...]内存数据集
      # 1-1 with open 读取文件数据集
      with open(data_path, 'r', encoding='utf-8') as f:
      my_lines = f.read().strip().split('\n')
      # print('my_lines --->', my_lines)
      # 1-2 获取 [[英文句子1, 法文句子1], [英文句子2, 法文句子2], ...] 数据集格式
      # 定义两个空列表
      tmp_pair, my_pairs = [], []
      # 循环遍历my_lines
      for line in my_lines:
      # print('line--->', line) # i m . j ai ans .
      # 对my_lines中每行样本使用\t分割符进行分割后再循环遍历
      for item in line.split('\t'):
      # print('item--->', item)
      # 将每行样本中的英文句子和法文句子使用工具函数进行清洗, 保存到tmp_pair列表中
      tmp_pair.append(normalizeString(item))
      # 将tmp_pair列表保存到my_pairs列表中
      my_pairs.append(tmp_pair)
      # 重置tmp_pair列表
      tmp_pair = []
      # print('my_pairs的长度为--->', len(my_pairs))
      # print('my_pairs[:4]--->', my_pairs[:4])

      # todo:2-构建英文和法文词表 {词:下标} {下标:词}
      # 2-0: 初始化词表, 有SOS和EOS两个词
      english_word2index = {'SOS':0, 'EOS':1}
      # 定义第3个词起始下标
      english_word_n = 2
      french_word2index = {'SOS': 0, 'EOS': 1}
      french_word_n = 2

      # 2-1: 循环遍历my_pairs [['i m .', 'j ai ans .'], ...]
      for pair in my_pairs:
      # print('pair--->', pair) # ['i m .', 'j ai ans .']
      # 2-2: 对英文句子或法文句子根据 ' '空格进行分割, 再进行循环遍历
      for word in pair[0].split(' '):
      # print('word--->', word) # i m .
      # 2-3: 使用if语句, 判断当前词是否在词表中, 如果不在添加进去
      if word not in english_word2index.keys():
      english_word2index[word] = english_word_n
      # 更新词下标
      english_word_n+=1
      for word in pair[1].split(' '):
      # 2-3: 使用if语句, 判断当前词是否在词表中, 如果不在添加进去
      if word not in french_word2index.keys():
      french_word2index[word] = french_word_n
      # 更新词下标
      french_word_n+=1

      # 2-4 获取{下标:词}格式词表
      english_index2word = {v:k for k, v in english_word2index.items()}
      french_index2word = {v:k for k, v in french_word2index.items()}
      # print('english_word2index--->', len(english_word2index), english_word2index)
      # print('french_word2index--->', len(french_word2index), french_word2index)
      # print('english_index2word--->', len(english_index2word), english_index2word)
      # print('french_index2word--->', len(french_index2word), french_index2word)
      # print('english_word_n--->', english_word_n)
      # print('french_word_n--->', french_word_n)
      return english_word2index, english_index2word, english_word_n, french_word2index, french_index2word, french_word_n, my_pairs


      if __name__ == '__main__':
      (english_word2index, english_index2word, english_word_n,
      french_word2index, french_index2word, french_word_n, my_pairs) = my_getdata()
    • 构建数据源对象

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      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
      56
      57
      58
      59
      60
      61
      62
      63
      # 自定义张量数据源类
      class MyPairsDataset(Dataset):
      # todo:1- init构造方法, 初始化属性
      def __init__(self, my_pairs, english_word2index, french_word2index):
      self.my_pairs = my_pairs # [[], [], ...]
      self.english_word2index = english_word2index
      self.french_index2word = french_word2index
      # 获取数据集长度
      self.sample_len = len(my_pairs)

      # todo:2- len方法, 返回数据集的长度
      def __len__(self):
      return self.sample_len

      # todo:3- getitem方法, 对数据进行处理, 转换成张量数据对象
      def __getitem__(self, index):
      """
      转换成张量数据对象
      :param index: 数据集的下标 -> 第index个样本
      :return: tensor_x, tensor_y
      """
      # 3-1: 修正index, 防止超过下标边界
      index = min(max(index, 0), self.sample_len - 1)
      # print('index--->', index)
      # 3-2: 获取当前index样本中的 x和y
      x = self.my_pairs[index][0]
      y = self.my_pairs[index][1]
      # print('x--->', x)
      # print('y--->', y)
      # 3-3: 将x和y的字符串数据转换成下标表示 词表
      # self.english_word2index[word]: 根据key获取字典中的value
      x = [self.english_word2index[word] for word in x.split(' ')]
      y = [self.french_index2word[word] for word in y.split(' ')]
      # print('x--->', x)
      # print('y--->', y)
      # 3-4: 每个样本最后加EOS下标 结束符号
      x.append(EOS_token)
      y.append(EOS_token)
      # print('x--->', x)
      # print('y--->', y)
      # 3-5: 将下标列表转换成张量对象
      # device: 将张量创建到对应的设备上 GPU/CPU
      tensor_x = torch.tensor(x, dtype=torch.long, device=device)
      tensor_y = torch.tensor(y, dtype=torch.long, device=device)
      # print('tensor_x--->', tensor_x)
      # print('tensor_y--->', tensor_y)
      return tensor_x, tensor_y


      if __name__ == '__main__':
      (english_word2index, english_index2word, english_word_n,
      french_word2index, french_index2word, french_word_n, my_pairs) = my_getdata()
      # 创建自定义数据源对象
      my_dataset = MyPairsDataset(my_pairs, english_word2index, french_word2index)
      print('my_dataset数据集条目数--->', len(my_dataset))
      print(my_dataset[0])
      # 创建数据加载器对象
      my_dataloader = DataLoader(dataset=my_dataset, batch_size=1, shuffle=True)
      # 循环遍历数据加载器
      for i, (x, y) in enumerate(my_dataloader):
      print('x--->', x.shape, x)
      print('y--->', y.shape, y)
      break

1.3.3 构建基于GRU的编码器和解码器

  • 构建基于GRU的编码器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    from preprocess import *
    class EncoderRNN(nn.Module):
    # todo:1- 定义构造方法 init
    def __init__(self, input_size, hidden_size):
    super().__init__()
    # 输入特征维度属性 input_size是英文词表的大小
    self.input_size = input_size
    # 词嵌入层和隐藏层特征维度属性 共用
    self.hidden_size = hidden_size
    # 词嵌入层对象属性
    self.embedding = nn.Embedding(num_embeddings=self.input_size,
    embedding_dim=self.hidden_size)
    # gru层对象属性
    # input_size: 上一层输出特征维度数
    # hidden_size: 当前层输出特征维度数
    # batch_first: x和hidden形状 -> (句子数, 句子长度, 词维度)
    self.gru = nn.GRU(input_size=self.hidden_size, hidden_size=self.hidden_size, batch_first=True)
    # todo:2- 定义前向传播方法 forward
    def forward(self, input, hidden):
    # print('input--->', input.shape)
    # 词嵌入操作 词向量化
    embedded = self.embedding(input)
    # print('embedded--->', embedded.shape)
    # gru层前向传播操作
    output, hn = self.gru(embedded, hidden)
    # print('output--->', output.shape)
    # print('hn--->', hn.shape)
    return output, hn
    # todo:3- 定义初始化隐藏状态值方法 inithidden
    def inithidden(self):
    return torch.zeros(size=(1, 1, self.hidden_size), device=device)


    if __name__ == '__main__':
    # 获取数据
    (english_word2index, english_index2word, english_word_n,
    french_word2index, french_index2word, french_word_n, my_pairs) = my_getdata()
    # 创建张量数据集
    my_dataset = MyPairsDataset(my_pairs, english_word2index, french_word2index)
    # 创建数据加载器
    # batch_size: 当前设置为1, 因为句子长度不一致
    my_dataloader = DataLoader(dataset=my_dataset, batch_size=1, shuffle=True)
    # 创建编码器对象
    my_encoderrnn = EncoderRNN(input_size=english_word_n, hidden_size=256).to(device=device)
    for i, (x, y) in enumerate(my_dataloader):
    # 一次性喂数据
    # 初始化隐藏状态值
    hidden = my_encoderrnn.inithidden()
    encoder_output, hn = my_encoderrnn(x, hidden)
    print('encoder_output--->', encoder_output.shape)
    print('hn--->', hn.shape)

    # 一个时间步一个时间步喂数据, gru底层实现 了解,解码器需要这样操作
    hidden = my_encoderrnn.inithidden()
    # x.shape[1]: 获取当前x的token数, 时间步数
    for j in range(x.shape[1]):
    # print('x--->', x)
    # print('x[0]--->', x[0])
    # print('x[0][j]--->', x[0][j])
    tmp_x = x[0][j].view(1, -1)
    print('tmp_x--->', tmp_x)
    output, hidden = my_encoderrnn(tmp_x, hidden)
    print('观察:最后一个时间步output输出是否相等') # hidden_size = 8 效果比较好
    print('encoder_output[0][-1]===>', encoder_output[0][-1])
    print('output===>', output)
    break
  • 构建基于GRU的解码器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    from encoderrnn import *

    class DecoderRNN(nn.Module):
    # todo:1- 定义构造方法 init
    def __init__(self, output_size, hidden_size):
    super().__init__()
    # 初始化法文词表大小维度属性=线性输出层的维度
    self.output_size = output_size
    # 初始化gru隐藏层和词嵌入层的维度属性 共用
    self.hidden_size = hidden_size
    # 初始化词嵌入层
    # num_embeddings: 法文词表大小
    # embedding_dim: 词向量初始维度
    self.embeding = nn.Embedding(num_embeddings=self.output_size, embedding_dim=self.hidden_size)
    # 初始化gru层
    self.gru = nn.GRU(input_size=self.hidden_size, hidden_size=self.hidden_size, batch_first=True)

    # 初始化全连接层 线性层+激活层
    # out_features: 法文词表大小 预测出n个词的生成概率
    self.out = nn.Linear(in_features=self.hidden_size, out_features=self.output_size)
    # dim:一定是-1, 按行处理
    self.softmax = nn.LogSoftmax(dim=-1)

    # todo:2- 定义前向传播方法 forward
    def forward(self, input, hidden):
    print('input--->', input.shape)
    # 词嵌入操作
    embedded = self.embeding(input)
    print('embedded--->', embedded.shape)
    # 通过relu激活函数引入非线性因素, 防止过拟合(x<0置为0, 神经元死亡)
    embedded = torch.relu(embedded)
    print('embedded--->', embedded.shape)
    # gru层操作
    # ouput: 输入input的语义信息, 形状为(句子数, 句子长度, 词维度) 三维
    output, hidden = self.gru(embedded, hidden)
    print('output--->', output.shape, output)
    # 全连接层操作
    # output[0]: 全连接层一般是二维数据, 所以要取出当前token的二维表示
    # 返回的output是 logsoftmax结果, 后续的值可能会有负值, 不是softmax的概率值
    output = self.softmax(self.out(output[0]))
    print('output--->', output.shape, output)
    return output, hidden


    if __name__ == '__main__':
    # 获取数据
    (english_word2index, english_index2word, english_word_n,
    french_word2index, french_index2word, french_word_n, my_pairs) = my_getdata()
    # 创建张量数据集
    my_dataset = MyPairsDataset(my_pairs, english_word2index, french_word2index)
    # 创建数据加载器
    # batch_size: 当前设置为1, 因为句子长度不一致
    my_dataloader = DataLoader(dataset=my_dataset, batch_size=1, shuffle=True)
    # 创建编码器对象
    my_encoderrnn = EncoderRNN(input_size=english_word_n, hidden_size=256).to(device=device)
    # 创建解码器对象
    output_size = french_word_n
    hidden_size = 256
    my_decoderrnn = DecoderRNN(output_size, hidden_size).to(device)
    for i, (x, y) in enumerate(my_dataloader):
    # 编码器进行编码 一次性喂数据
    # 初始化隐藏状态值
    hidden = my_encoderrnn.inithidden()
    encoder_output, hn = my_encoderrnn(x, hidden)
    print('encoder_output--->', encoder_output.shape)
    print('hn--->', hn.shape, hn)

    # 解码器进行解码, 自回归, 一个一个token进行解码
    for j in range(y.shape[1]) :
    # 获取当前预测token时间步的输入x(等同于上一时间步的预测y)
    # 当前以真实y中的每个token作为输入, 模拟解码器的界面过程, 实际上第一个输入token一定是起始符号
    tmp_y = y[0][j].view(1, -1)
    # 进行解码
    # 初始的隐藏状态值=编码器最后一个时间步的隐藏状态值
    my_decoderrnn(tmp_y, hn)
    break
    break
  • 构建基于GRU和Attention的解码器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    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
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    # 带加性注意力机制的解码器
    class AttnDecoderRNN(nn.Module):
    # todo:1- 定义构造方法 init
    def __init__(self, output_size, hidden_size, dropout_p=0.2, max_length=MAX_LENGTH):
    super().__init__()
    # 初始化词嵌入层的输入维度和全连接层的输出维度一致
    self.output_size = output_size
    # 初始化编码器解码器隐藏层维度属性 解码器的第一个隐藏状态值=编码器的最后一个隐藏状态值
    # 初始化词嵌入层维度属性 共享
    self.hidden_size = hidden_size
    # 初始化最大句子长度属性 -> 所有句子 c的长度固定
    self.max_length = max_length
    # 初始化dropout概率属性
    self.dropout_p = dropout_p
    # 初始化 embeding层
    self.embedding = nn.Embedding(num_embeddings=self.output_size, embedding_dim=self.hidden_size)
    # 初始化 gru层
    self.gru = nn.GRU(input_size=self.hidden_size, hidden_size=self.hidden_size, batch_first=True)
    # 初始化 全连接层
    self.out = nn.Linear(in_features=self.hidden_size, out_features=self.output_size)
    self.softmax = nn.LogSoftmax(dim=-1)

    # 初始化注意力机制中两个线性层
    """
    q:解码器当前预测时间步的隐藏状态值
    k:解码器当前预测时间步的上一时间步隐藏状态值
    v:编码器的output输出
    q,k,v三个特征维度相同 都是hidden_size
    """
    # in_features: q和k的特征维度拼接
    # out_features: 后续权重概率矩阵->(1, 1, max_len) 和 V矩阵相乘 V->(1, max_len, hidden_size)
    self.attn = nn.Linear(in_features=self.hidden_size + self.hidden_size, out_features=self.max_length)
    # in_features: q和c的特征维度拼接
    # out_features: 输出的维度和gru层的输入维度保持一致
    self.attn_combine = nn.Linear(in_features=self.hidden_size + self.hidden_size, out_features=self.hidden_size)
    # 初始化dropout层
    self.dropout = nn.Dropout(p=self.dropout_p)

    # todo:2- 定义前向传播方法 forward
    def forward(self, input, hidden, encoder_outputs):
    """
    前向传播计算
    :param input: q, 解码器当前预测时间步的输入x, 也是上一个时间步预测的输出y
    :param hidden: k, 上一个时间步的隐藏状态值, 第一个时间步的上一个隐藏状态值=编码器最后一个时间步的隐藏状态值
    :param encoder_outputs: v, 编码器的输出 output, 后续是统一长度都为10, 10个token, 不足10个token用0填充
    :return: 预测词表概率向量, 当前时间步的隐藏状态值, 权重概率矩阵
    """
    # 2-1 词嵌入操作
    embedded = self.embedding(input)
    # 使用dropout防止过拟合
    embedded = self.dropout(embedded)
    print('embedded--->', embedded.shape, embedded)

    # 2-2 计算权重分数矩阵, 之后再计算权重概率矩阵
    # q和k在特征维度轴拼接 + 线性计算 + softmax计算
    # embedded[0]: 获取二维向量表示, 线性层一般接收二维数据
    attn_weights = torch.softmax(self.attn(torch.cat(tensors=[embedded[0], hidden[0]], dim=1)), dim=-1)
    print('attn_weights--->', attn_weights.shape, attn_weights)
    # print(torch.sum(input=attn_weights))

    # 2-3 计算动态c, 加权求和 权重概率矩阵和v进行三维矩阵乘法
    # bmm() 三维矩阵乘法, 目前attn_weights和encoder_outputs二维矩阵
    attn_applied = torch.bmm(attn_weights.unsqueeze(0), encoder_outputs.unsqueeze(0))
    print('attn_applied--->', attn_applied.shape, attn_applied)

    # 2-4 q和动态c融合线性计算, 得到gru的输入x
    # unsqueeze():得到三维数据, gru的输入x的形状要求
    output = self.attn_combine(torch.cat(tensors=[embedded[0], attn_applied[0]], dim=1)).unsqueeze(0)
    print('output--->', output.shape, output)
    # relu激活函数, 非线性因素
    output = torch.relu(output)

    # 2-5 gru层操作
    output, hidden = self.gru(output, hidden)
    print('output--->', output.shape, output)
    print('hidden--->', hidden.shape, hidden)

    # 2-6 全连接层操作
    output = self.softmax(self.out(output[0]))
    print('output--->', output.shape, output)
    return output, hidden, attn_weights


    if __name__ == '__main__':
    # 获取数据
    (english_word2index, english_index2word, english_word_n,
    french_word2index, french_index2word, french_word_n, my_pairs) = my_getdata()
    # 创建张量数据集
    my_dataset = MyPairsDataset(my_pairs, english_word2index, french_word2index)
    # 创建数据加载器
    # batch_size: 当前设置为1, 因为句子长度不一致
    my_dataloader = DataLoader(dataset=my_dataset, batch_size=1, shuffle=True)
    # 创建编码器对象
    my_encoderrnn = EncoderRNN(input_size=english_word_n, hidden_size=256).to(device=device)
    # 创建解码器对象
    output_size = french_word_n
    hidden_size = 256
    # my_decoderrnn = DecoderRNN(output_size, hidden_size).to(device)

    # 创建带attn的解码器对象
    my_attndecoderrnn = AttnDecoderRNN(output_size, hidden_size).to(device)
    for i, (x, y) in enumerate(my_dataloader):
    # print('x--->', x.shape)
    # 编码器进行编码 一次性喂数据
    # 初始化隐藏状态值
    hidden = my_encoderrnn.inithidden()
    encoder_output, hn = my_encoderrnn(x, hidden)
    print('encoder_output--->', encoder_output.shape, encoder_output)
    # print('hn--->', hn.shape, hn)

    # 获取填充成最大程度的编码器c或者output
    # 初始化全0的张量 形状(10, 256) [[0,0,0,0,0,0,...],[],[]]
    encoder_output_c = torch.zeros(size=(MAX_LENGTH, my_encoderrnn.hidden_size), device=device)
    # 将encoder_output真实值赋值到encoder_output_c对应位置
    for idx in range(x.shape[1]):
    encoder_output_c[idx] = encoder_output[0][idx]
    print('encoder_output_c--->', encoder_output_c.shape, encoder_output_c)
    # 解码器进行解码, 自回归, 一个一个token进行解码
    for j in range(y.shape[1]):
    # 获取当前预测token时间步的输入x(等同于上一时间步的预测y)
    # 当前以真实y中的每个token作为输入, 模拟解码器的界面过程, 实际上第一个输入token一定是起始符号
    tmp_y = y[0][j].view(1, -1)
    # 进行解码
    # 初始的隐藏状态值=编码器最后一个时间步的隐藏状态值
    # my_decoderrnn(tmp_y, hn)
    # hn:编码器端最后一个时间步的隐藏状态值, 也是解码器端第一个时间步的初始的隐藏状态值
    print('hn--->', hn.shape, hn)
    output, hidden, attn_weights = my_attndecoderrnn(tmp_y, hn, encoder_output_c)
    print('=' * 80)
    print('output--->', output.shape, output)
    print('hidden--->', hidden.shape, hidden)
    print('attn_weights--->', attn_weights.shape, attn_weights)
    break
    break

注意力机制应用

注意力机制应用

1 注意力机制应用

  • 思路: 解码器端的一般注意力机制(加性注意力)

  • 实现步骤:

    • q和k按特征维度轴进行拼接torch.concat(), 经过线性层计算nn.linear(), 再经过softmax激活层计算torch.softmax(, dim=-1), 得到权重概率矩阵
    • 将上一步的权重概率矩阵和V进行矩阵乘法torch.bmm(), 得到动态张量c
    • q和动态张量c进行融合, 按特征维度轴进行拼接torch.concat(), 经过线性层计算nn.linear(), 得到融合的结果->解码器当前时间步的输入X output, hn=nn.gru(X, h0)
  • 代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    import torch
    import torch.nn as nn


    # 创建神经网络类
    class Attn(nn.Module):
    # todo:1-初始化init构造函数
    def __init__(self, query_size, key_size, value_size1, value_size2, output_size):
    super().__init__()
    # 1-1: 初始化对象属性
    self.query_size = query_size # 解码器隐藏状态值的特征维度 或 输入x的特征维度
    self.key_size = key_size # 编码器隐藏状态值的特征维度 解码器第一个隐藏状态=编码器最后一个隐藏状态
    # value-> 编码器的output结果 (batch_size, seq_len, hidden_size)
    self.value_size1 = value_size1 # seq_len, 句子长度, 时间步数量, token数
    self.value_size2 = value_size2 # hidden_size, gru层提取的特征维度
    self.output_size = output_size # 解码器输入的特征维度
    # 1-2: 定义线性层1 -> q和k在特征维度轴拼接后进行线性计算
    # 输入特征数/输入神经元个数: self.query_size + self.key_size q和k在特征维度轴拼接
    # 输出特征数/输出神经元个数: self.value_size1
    # 权重概率矩阵 * value = (b, n, m) * (b, m, p) = (b, n, p) (b,n,p)代表是动态c的形状
    # value->(b, m, p) b=1, m=value_size1 p=value_size2
    # 权重概率矩阵的形状是由 attn线性层决定 (b, n, m) -> m=value_size1
    self.attn = nn.Linear(self.query_size + self.key_size, self.value_size1)
    # 1-3: 定义线性层2 -> q和动态c在特征维度轴拼接后进行线性计算, 得到当前时间步的输入X(X=w(q+c))
    # self.query_size + self.value_size2->为什么加value_size2?
    # q和动态c在特征维度上拼接,而c形状->(b, m, p) p=value_size2
    self.attn_combine = nn.Linear(self.query_size + self.value_size2, self.output_size)

    # todo:2- 定义forward方法
    def forward(self, Q, K, V):
    # print('Q--->', Q.shape, Q)
    # print('K--->', K.shape, K)
    # print('V--->', V.shape, V)
    # 2-1 拼接+线性计算+softmax -> 权重概率矩阵
    # print('Q[0]--->', Q[0].shape, Q[0]) # 形状->(句子长度, 词向量维度)
    # dim=1:特征维度进行拼接
    # print('q和k拼接结果--->', torch.cat(tensors=[Q[0], K[0]], dim=1).shape)
    # print('q和k拼接后线性计算结果--->', self.attn(torch.cat(tensors=[Q[0], K[0]], dim=1)).shape)
    # dim=-1:按行进行softmax计算
    attn_weights = torch.softmax(self.attn(torch.cat(tensors=[Q[0], K[0]], dim=1)), dim=-1)
    print('权重概率矩阵--->', attn_weights.shape, attn_weights)
    # 2-2 权重概率矩阵和V矩阵相乘 -> 动态张量c
    # attn_weights.unsqueeze(0)->将二维转换成三维 (句子数, 句子长度, 词向量维度)
    attn_applied = torch.bmm(input=attn_weights.unsqueeze(0), mat2=V)
    print('动态张量c--->', attn_applied.shape, attn_applied)
    # 2-3 Q和动态张量c融合 -> 得到 解码器当前时间步的输入X, 后续将X输入到gru层 nn.GRU(X, h0)
    q_c_cat = torch.cat(tensors=[Q[0], attn_applied[0]], dim=1)
    print('q和动态张量c融合结果--->', q_c_cat.shape, q_c_cat)
    # gru层的输入X是三维, 所以进行unsqueeze(0)
    output = self.attn_combine(q_c_cat).unsqueeze(0)
    print('gru当前时间步的输入X(q和c融合结果)--->', output.shape, output)
    # outpu后续输入到gru层
    # 伪代码: output2, hn = nn.GRU(output, h0)
    return output, attn_weights


    if __name__ == '__main__':
    # 定义特征维度
    query_size = 32
    key_size = 32
    value_size1 = 32
    value_size2 = 64
    output_size = 32

    # 创建测试q,k,v
    # 形状 -> (batch_size, seq_len, embedding)
    Q = torch.randn(1, 1, query_size)
    K = torch.randn(1, 1, key_size)
    V = torch.randn(1, value_size1, value_size2)

    # 创建实例对象
    my_attn = Attn(query_size, key_size, value_size1, value_size2, output_size)
    # 调用实例对象, 自动执行forward方法
    output, attn_weights = my_attn(Q, K, V)
    print('=' * 80)
    print('输出结果--->', output.shape, output)
    print('权重概率矩阵--->', attn_weights.shape, attn_weights)

2 RNN案例-seq2seq英译法

2.1 seq2seq模型介绍

  • 模型结构
    • 编码器 encoder
    • 解码器 decoder
    • 编码器和解码器中可以使用RNN模型或者是transformer模型
  • 工作流程
    • 编码器生成上下文语义张量 -> 什么是nlp? 将问题转换成语义张量
    • 解码器根据编码器的语义张量和上一时间步的预测值以及上一时间步的隐藏状态值进行当前时间步的预测
      • 自回归模式
  • 局限性
    • 信息瓶颈问题
    • 长序列问题

2.2 数据集介绍

1748849163083

  • 每行样本由英文句子和法文句子对组成, 中间用\t分隔开
  • 英文句子是编码器的输入序列, 法文句子是解码器的输出序列(预测序列)对应的真实序列

2.3 案例实现步骤

2.3.1 文本清洗工具函数

  • utils.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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
    # 用于正则表达式
    import re
    # 用于构建网络结构和函数的torch工具包
    import torch
    import torch.nn as nn
    from torch.utils.data import Dataset, DataLoader
    # torch中预定义的优化方法工具包
    import torch.optim as optim
    import time
    # 用于随机生成数据
    import random
    import numpy as np
    import matplotlib.pyplot as plt

    # 定义变量
    # 选择设备 cpu/gpu
    # 'cuda'->使用所有显卡 'cuda:0'->使用第一张显卡
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    # 起始符号下标
    # sos -> start of sentences
    SOS_token = 0
    # 结束符号下标
    EOS_token = 1
    # 文件路径
    data_path = 'data/eng-fra-v2.txt'
    # 最大句子长度, 预处理分析的结果
    MAX_LENGTH = 10


    # 定义处理文本的工具函数 处理句子中的特殊符号/大小写/换行符
    def normalizeString(s: str):
    # 转换成小写, 并删掉两端的空白符号
    str = s.lower().strip()
    # 正则表达式匹配标签符号'.?!' 转换成 ' .?!'
    str = re.sub(r'([.!?])', r' \1', str)
    # print('str--->', str)
    # 正则表达式匹配除a-z.!?之外的其他的符号 转换成 ' '
    str = re.sub(r'[^a-z.!?]+', r' ', str)
    # print('str--->', str)
    return str


    if __name__ == '__main__':
    str1 = 'I m sad.@'
    normalizeString(str1)

2.3.2 数据预处理

  • preprocess.py
    • 清洗文本和构建词表

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      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
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      from utils import *


      def my_getdata():
      # todo:1- 读取文件数据集, 得到 [[英文句子1, 法文句子1], [英文句子2, 法文句子2], ...]内存数据集
      # 1-1 with open 读取文件数据集
      with open(data_path, 'r', encoding='utf-8') as f:
      my_lines = f.read().strip().split('\n')
      # print('my_lines --->', my_lines)
      # 1-2 获取 [[英文句子1, 法文句子1], [英文句子2, 法文句子2], ...] 数据集格式
      # 定义两个空列表
      tmp_pair, my_pairs = [], []
      # 循环遍历my_lines
      for line in my_lines:
      # print('line--->', line) # i m . j ai ans .
      # 对my_lines中每行样本使用\t分割符进行分割后再循环遍历
      for item in line.split('\t'):
      # print('item--->', item)
      # 将每行样本中的英文句子和法文句子使用工具函数进行清洗, 保存到tmp_pair列表中
      tmp_pair.append(normalizeString(item))
      # 将tmp_pair列表保存到my_pairs列表中
      my_pairs.append(tmp_pair)
      # 重置tmp_pair列表
      tmp_pair = []
      # print('my_pairs的长度为--->', len(my_pairs))
      # print('my_pairs[:4]--->', my_pairs[:4])

      # todo:2-构建英文和法文词表 {词:下标} {下标:词}
      # 2-0: 初始化词表, 有SOS和EOS两个词
      english_word2index = {'SOS':0, 'EOS':1}
      # 定义第3个词起始下标
      english_word_n = 2
      french_word2index = {'SOS': 0, 'EOS': 1}
      french_word_n = 2

      # 2-1: 循环遍历my_pairs [['i m .', 'j ai ans .'], ...]
      for pair in my_pairs:
      # print('pair--->', pair) # ['i m .', 'j ai ans .']
      # 2-2: 对英文句子或法文句子根据 ' '空格进行分割, 再进行循环遍历
      for word in pair[0].split(' '):
      # print('word--->', word) # i m .
      # 2-3: 使用if语句, 判断当前词是否在词表中, 如果不在添加进去
      if word not in english_word2index.keys():
      english_word2index[word] = english_word_n
      # 更新词下标
      english_word_n+=1
      for word in pair[1].split(' '):
      # 2-3: 使用if语句, 判断当前词是否在词表中, 如果不在添加进去
      if word not in french_word2index.keys():
      french_word2index[word] = french_word_n
      # 更新词下标
      french_word_n+=1

      # 2-4 获取{下标:词}格式词表
      english_index2word = {v:k for k, v in english_word2index.items()}
      french_index2word = {v:k for k, v in french_word2index.items()}
      # print('english_word2index--->', len(english_word2index), english_word2index)
      # print('french_word2index--->', len(french_word2index), french_word2index)
      # print('english_index2word--->', len(english_index2word), english_index2word)
      # print('french_index2word--->', len(french_index2word), french_index2word)
      # print('english_word_n--->', english_word_n)
      # print('french_word_n--->', french_word_n)
      return english_word2index, english_index2word, english_word_n, french_word2index, french_index2word, french_word_n, my_pairs


      if __name__ == '__main__':
      (english_word2index, english_index2word, english_word_n,
      french_word2index, french_index2word, french_word_n, my_pairs) = my_getdata()
    • 构建数据源对象

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      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
      56
      57
      58
      59
      60
      61
      62
      63
      # 自定义张量数据源类
      class MyPairsDataset(Dataset):
      # todo:1- init构造方法, 初始化属性
      def __init__(self, my_pairs, english_word2index, french_word2index):
      self.my_pairs = my_pairs # [[], [], ...]
      self.english_word2index = english_word2index
      self.french_index2word = french_word2index
      # 获取数据集长度
      self.sample_len = len(my_pairs)

      # todo:2- len方法, 返回数据集的长度
      def __len__(self):
      return self.sample_len

      # todo:3- getitem方法, 对数据进行处理, 转换成张量数据对象
      def __getitem__(self, index):
      """
      转换成张量数据对象
      :param index: 数据集的下标 -> 第index个样本
      :return: tensor_x, tensor_y
      """
      # 3-1: 修正index, 防止超过下标边界
      index = min(max(index, 0), self.sample_len - 1)
      # print('index--->', index)
      # 3-2: 获取当前index样本中的 x和y
      x = self.my_pairs[index][0]
      y = self.my_pairs[index][1]
      # print('x--->', x)
      # print('y--->', y)
      # 3-3: 将x和y的字符串数据转换成下标表示 词表
      # self.english_word2index[word]: 根据key获取字典中的value
      x = [self.english_word2index[word] for word in x.split(' ')]
      y = [self.french_index2word[word] for word in y.split(' ')]
      # print('x--->', x)
      # print('y--->', y)
      # 3-4: 每个样本最后加EOS下标 结束符号
      x.append(EOS_token)
      y.append(EOS_token)
      # print('x--->', x)
      # print('y--->', y)
      # 3-5: 将下标列表转换成张量对象
      # device: 将张量创建到对应的设备上 GPU/CPU
      tensor_x = torch.tensor(x, dtype=torch.long, device=device)
      tensor_y = torch.tensor(y, dtype=torch.long, device=device)
      # print('tensor_x--->', tensor_x)
      # print('tensor_y--->', tensor_y)
      return tensor_x, tensor_y


      if __name__ == '__main__':
      (english_word2index, english_index2word, english_word_n,
      french_word2index, french_index2word, french_word_n, my_pairs) = my_getdata()
      # 创建自定义数据源对象
      my_dataset = MyPairsDataset(my_pairs, english_word2index, french_word2index)
      print('my_dataset数据集条目数--->', len(my_dataset))
      print(my_dataset[0])
      # 创建数据加载器对象
      my_dataloader = DataLoader(dataset=my_dataset, batch_size=1, shuffle=True)
      # 循环遍历数据加载器
      for i, (x, y) in enumerate(my_dataloader):
      print('x--->', x.shape, x)
      print('y--->', y.shape, y)
      break

卷积神经网络CNN

1 图像基础知识

1.1 图像基本概念

图像是人类视觉的基础,是自然景物的客观反映,是人类认识世界和人类本身的重要源泉。“图”是物体反射或透射光的分布,“像“是人的视觉系统所接受的图在人脑中所形成的印象或认识照片、绘画、剪贴画、地图、书法作品、手写汉字、传真、卫星云图、影视画面、X光片、脑电图、心电图等都是图像。

在计算机中,按照颜色和灰度的多少可以将图像分为四种基本类型。

  • 二值图像

    一幅二值图像的二维矩阵仅由0、1两个值构成,“0”代表黑色,“1”代白色。由于每一像素(矩阵中每一元素)取值仅有0、1两种可能,所以计算机中二值图像的数据类型通常为1个二进制位。二值图像通常用于文字、线条图的扫描识别(OCR)和掩膜图像的存储。

    1755689633198

  • 灰度图像

    灰度图像矩阵元素的取值范围通常为[0,255]。因此其数据类型一般为8位无符号整数的(int8),这就是人们经常提到的256灰度图像。**“0”表示纯黑色,“255”表示纯白色,中间的数字从小到大表示由黑到白的过渡色。**二值图像可以看成是灰度图像的一个特例。

  • 索引图像

    索引图像的文件结构比较复杂,除了存放图像的二维矩阵外,还包括一个称之为颜色索引矩阵MAP的二维数组。MAP的大小由存放图像的矩阵元素值域决定,如矩阵元素值域为[0,255],则MAP矩阵的大小为256Ⅹ3,用MAP=[RGB]表示MAP中每一行的三个元素分别指定该行对应颜色的红、绿、蓝单色值,MAP中每一行对应图像矩阵像素的一个灰度值,如某一像素的灰度值为64,则该像素就与MAP中的第64行建立了映射关系,该像素在屏幕上的实际颜色由第64行的[RGB]组合决定。也就是说,图像在屏幕上显示时,每一像素的颜色由存放在矩阵中该像素的灰度值作为索引通过检索颜色索引矩阵MAP得到。

    1755689642971

  • 真彩色RGB图像

    RGB图像与索引图像一样都可以用来表示彩色图像。与索引图像一样,它分别用红(R)、绿(G)、蓝(B)三原色的组合来表示每个像素的颜色。但与索引图像不同的是,RGB图像每一个像素的颜色值(由RGB三原色表示)直接存放在图像矩阵中,由于每一像素的颜色需由R、G、B三个分量来表示,**M、N分别表示图像的行列数,三个M x N的二维矩阵分别表示各个像素的R、G、B三个颜色分量。**RGB图像的数据类型一般为8位无符号整形。注意:通道的顺序是 BGR 而不是 RGB。

    1755689790662

    1755689804624

图像类型 通道数 像素值范围 主要特点 常见用途
二值图像 1通道 0 或 1 每个像素只有黑与白两种值 形态学操作、二值化、轮廓检测
灰度图像 1通道 0 到 255 每个像素表示灰度(亮度) 图像预处理、物体检测、人脸识别
索引图像 1通道 0 到 255(索引) 像素值为颜色表的索引,颜色表决定实际颜色 存储压缩、较少颜色的图像表示
RGB图像 3通道(R、G、B) 0 到 255 每个像素由红、绿、蓝三个通道组成 普通彩色图像显示、图像处理与分析

简单的讲:图像是由像素点组成的,每个像素点的取值范围为: [0, 255] 。像素值越接近于0,颜色越暗,接近于黑色;像素值越接近于255,颜色越亮,接近于白色。

在深度学习中,我们使用的图像大多是彩色图,彩色图由RGB3个通道组成,如下图所示:

1755689839501

1.2 图像加载

使用 matplotlib 库来实际理解下上面讲解的图像知识。

1
2
3
4
5
6
7
8
9
10
11
12
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
import numpy as np
import matplotlib.pyplot as plt


# 像素值的理解
def test01():
# 全0数组是黑色的图像
# H, W, C -> 高, 宽, 通道
img = np.zeros(shape=[200, 200, 3])
# 展示图像
plt.imshow(img)
# 对坐标轴进行设置
# off:关闭坐标轴
plt.axis("off")
plt.show()

# 全255数组是白色的图像
img = np.full(shape=[200, 200, 3], fill_value=255)
# 展示图像
plt.imshow(img)
plt.show()


# 图像的加载
def test02():
# 读取图像
img = plt.imread("data/img.jpg")
# 保存图像
plt.imsave("data/img1.jpg", img)
# 打印图像形状 高,宽,通道
print("图像的形状(H, W, C):\n", img.shape)
# 展示图像
plt.imshow(img)
plt.axis("off")
plt.show()


if __name__ == '__main__':
test01()
test02()

输出结果:

全黑和全白图像:

1755689851639

图像的形状为:

1
2
图像的形状(H,W,C):
(640, 640, 3)

1755689859599

2 卷积神经网络(CNN)概述

2.1 什么是卷积神经网络

卷积神经网络是深度学习在计算机视觉领域的突破性成果,专门用于处理图像、视频、语音等数据的神经网络

在计算机视觉领域, 往往我们输入的图像都很大,使用全连接网络的话,计算的代价较高。另外图像也很难保留原有的特征,导致图像处理的准确率不高。

卷积神经网络(Convolutional Neural Network)是含有卷积层的神经网络。卷积层的作用就是用来自动学习、提取图像的特征

CNN网络主要由三部分构成:卷积层、池化层和全连接层构成:

(1)卷积层负责提取图像中的局部特征

(2)池化层用来大幅降低参数量级(降维)

(3)全连接层类似人工神经网络的部分,用来输出想要的结果

1755689873488

上图中CNN要做的事情是:给定一张图片,是车还是马未知,是什么车也未知,现在需要模型判断这张图片里具体是一个什么东西,总之输出一个结果:如果是车,那是什么车

  • 最左边是
    • 数据输入层:对数据做一些处理,比如去均值(各维度都减对应维度的均值,使得输入数据各个维度都中心化为0,避免数据过多偏差,影响训练效果)、归一化(把所有的数据都归一到同样的范围)、PCA等等。CNN只对训练集做“去均值”这一步。
  • 中间是
    • 卷积层(CONV):线性乘积求和,提取图像中的局部特征
    • 激励层(RELU):ReLU激活函数,输入数据转换成输出数据
    • 池化层(POOL):取区域平均值或最大值,大幅降低参数量级(降维)
  • 最右边是
    • 全连接层(FC):接收二维数据集,输出CNN模型预测结果

2.2 卷积神经网络应用

图像分类:最常见的应用,例如识别图片中的物体类别

目标检测:检测图像中物体的位置和类别

图像分割:将图像分成多个区域,用于语义分割

人脸识别:识别图像中的人脸

医学图像分析:用于检测医学图像中的异常(如癌症检测、骨折检测等)

自动驾驶:用于识别交通标志、车辆、行人

2.3 CNN中的经典算法/网络架构

**LeNet-5:**作为最早的CNN架构之一,证明了CNN在图像识别任务上的有效性,为后续的CNN发展奠定了基础

  • 卷积层: 提取图像的边缘、角点等基本特征
  • 池化层 (子采样层): 降低特征图的维度,减少计算量,并提高模型对输入图像微小变化的鲁棒性
  • 全连接层: 将卷积层和池化层提取的特征进行组合,用于最终的分类

**AlexNet:**显著提升了ImageNet图像分类的准确率,证明了深度学习在计算机视觉领域的潜力,并推动了深度学习的快速发展

  • 卷积层: 使用更大的卷积核和更多的卷积核,提取更丰富的图像特征
  • ReLU激活函数: 加速训练过程,并提高模型的性能
  • 最大池化层: 降低特征图的维度
  • Dropout层: 防止过拟合
  • 全连接层: 用于最终的分类

**VGGNet:**探索了网络深度对性能的影响,证明了更深的网络可以提取更抽象和更具表达力的特征

  • 卷积层: 使用更小的卷积核 (3x3),并堆叠多个卷积层,增加了网络的深度,提取更复杂的特征
  • 最大池化层: 降低特征图的维度
  • 全连接层: 用于最终的分类

**GoogLeNet (Inception):**提出了 Inception 模块,在提高性能的同时减少了计算量,为后续的网络架构设计提供了新的思路

  • Inception 模块: 并行使用不同大小的卷积核和池化操作,然后将它们的输出连接起来,增加了网络的宽度,提高了网络的效率

**ResNet:**解决了深度网络训练困难的问题,使得可以训练更深的网络,从而显著提高了模型的性能

  • 残差块 (Residual Block): 引入跳跃连接 (Shortcut Connection),允许梯度直接反向传播到浅层,解决了深度网络的梯度消失问题,使得训练非常深的网络成为可能

DenseNet:

  • 密集块 (Dense Block): 将每一层都与之前的所有层连接,特征重用更加充分,进一步提高了网络的性能和参数效率
  • DenseNet通过密集连接(Dense Connectivity)在网络中各层之间建立了直接的连接,即每一层都接收前面所有层的输出作为输入。这种设计增强了特征传递和梯度流动,避免了梯度消失问题,并提高了信息的利用率

3 卷积层

卷积层(Convolutional Layer)通过卷积操作提取输入数据中的特征(例如图像中的边缘、纹理、形状等)。

卷积层利用卷积核(滤波器)对输入进行处理,从而生成特征图(feature map),并且每个卷积层能够提取不同层次的特征,从低级特征(如边缘)到高级特征(如物体的形状)。

卷积层的主要作用如下:

  • 特征提取:卷积层的主要作用是从输入图像中提取低级特征(如边缘、角点、纹理等)。通过多个卷积层的堆叠,网络能够逐渐从低级特征到高级特征(如物体的形状、区域等)进行学习。

  • 权重共享:在卷积层中,同一个卷积核在整个输入图像上共享权重,这使得卷积层的参数数量大大减少,减少了计算量并提高了训练效率。

  • 局部连接:卷积层中的每个神经元仅与输入图像的一个小局部区域相连,这称为局部感受野,这种局部连接方式更符合图像的空间结构,有助于捕捉图像中的局部特征。

  • 空间不变性:由于卷积操作是局部的并且采用权重共享,卷积层在处理图像时具有平移不变性。也就是说,不论物体出现在图像的哪个位置,卷积层都能有效地检测到这些物体的特征。

3.1 卷积计算

1755689889238

  1. input 表示输入的图像

  2. filter 表示卷积核, 也叫做滤波器(滤波矩阵)

    • 一组固定的权重,因为每个神经元的多个权重固定,所以又可以看做一个恒定的滤波器filter
    • 非严格意义上来讲,下图中红框框起来的部分便可以理解为一个滤波器,即带着一组固定权重的神经元。多个滤波器叠加便成了卷积层
    • 一个卷积核就是一个神经元

    1755689896533

  3. input 经过 filter 得到输出为最右侧的图像,该图叫做特征图

那么, 它是如何进行计算的呢?卷积运算本质上就是在滤波器和输入数据的局部区域间做点积。

1755689907142

左上角的点计算方法:

1755689915142

按照上面的计算方法可以得到最终的特征图为:

1755689925863

图像上的卷积:

在下图对应的计算过程中,输入是一定区域大小(width*height)的数据,和滤波器filter(带着一组固定权重的神经元)做内积后得到新的二维数据。

1755689935118

具体来说,左边是图像输入,中间部分就是滤波器filter(带着一组固定权重的神经元),不同的滤波器filter会得到不同的输出数据,比如颜色深浅、轮廓。相当于如果想提取图像的不同特征,则用不同的滤波器filter,提取想要的关于图像的特定信息:颜色深浅或轮廓。

3.2 Padding(填充)

通过上面的卷积计算过程,最终的特征图比原始图像小很多,如果想要保持经过卷积后的图像大小不变, 可以在原图周围添加 Padding 来实现。

Padding(填充)操作是一种用于==在输入特征图的边界周围添加额外像素(通常是零)==。

Padding的主要作用:

  • **保持空间维度:**如果不使用 padding,每次卷积操作后,特征图的尺寸都会缩小。多次卷积后,特征图会变得非常小,可能会丢失重要的边缘信息。Padding可以帮助维持输出特征图的尺寸与输入相同或接近相同。
  • **保留边缘信息:**图像边缘的像素在卷积过程中参与的计算次数较少,这意味着边缘信息在特征提取过程中容易丢失。Padding通过在边缘添加额外的像素,增加了边缘像素的参与度,从而更好地保留了边缘信息。
  • **提高性能:**Padding有助于避免由于特征图尺寸快速缩小而导致的信息丢失,从而提高模型的性能,尤其是在处理较小的图像或需要进行多层卷积时。

Padding的类型:

  • Valid Padding (No Padding): 不进行任何填充。卷积核只在输入图像的有效区域内滑动。输出尺寸会缩小。
  • Same Padding: 添加足够的填充,使得输出特征图的尺寸与输入相同。
  • Full Padding: 尽可能多地添加填充,使得卷积核的每个元素都至少在输入图像上滑动一次。输出尺寸会增大。

**Padding的选择:**取决于具体的应用场景和网络架构

  • Valid Padding: 适用于不需要保持输出尺寸的场景,或者输入图像足够大,边缘信息丢失不重要的情况。
  • Same Padding: 广泛应用于各种CNN架构中,因为它可以保持特征图的尺寸,方便网络设计和计算。
  • Full Padding: 较少使用,因为它会增加计算量,并且可能会在边缘引入一些伪影。

1755689952791

3.3 Stride(步长)

Stride(步长)指的是卷积核在图像上滑动时的步伐大小,即每次卷积时卷积核在图像中向右(或向下)移动的像素数。步长直接影响卷积操作后输出特征图的尺寸,以及计算量和模型的特征提取能力。

Stride的作用:

  • **降低计算复杂度:**更大的步长意味着卷积核移动的次数更少,从而减少了计算量,并加快了训练和推理速度。
  • **减小特征图尺寸:**步长越大,生成的特征图尺寸越小。这类似于池化的降维效果。
  • **增大感受野:**虽然更大的步长会减小特征图的尺寸,但它同时也会增大每个神经元在输入数据上的感受野。这意味着每个神经元能够捕捉到更大范围的输入信息。

**Stride的选择:**取决于具体的应用场景和网络架构

  • Stride = 1: 这是最常见的设置,尤其是在网络的早期层。它允许保留更多的空间细节。
  • Stride > 1: 通常用于减小特征图的尺寸和增大感受野,例如在网络的后期层或需要进行快速降维时。 常见的设置包括 stride=2 或 stride=4。

按照步长为1来移动卷积核,计算特征图如下所示:

1755689966517

如果把Stride增大为2,也是可以提取特征图的,如下图所示:

1755689973014

3.4 多通道卷积计算

实际中的图像都是多个通道组成的,我们怎么计算卷积呢?

1755689982891

计算方法如下:

  1. 当输入有多个通道(Channel), 例如 RGB 三个通道, 此时要求卷积核需要拥有相同的通道数(图像有多少通道,每个卷积核就有多少通道).
  2. 每个卷积核通道与对应的输入图像的各个通道进行卷积.
  3. 将每个通道的卷积结果按位相加得到最终的特征图.

如下图所示:

1755689992907

3.5 多卷积核卷积计算

上面的例子里我们只使用一个卷积核进行特征提取, 实际对图像进行特征提取时, 我们需要使用多个卷积核进行特征提取. 这个多个卷积核可以理解为从不同到的视角、不同的角度对图像特征进行提取.

那么, 当使用多个卷积核时, 应该怎么进行特征提取呢?

1755690006300

通过以下例子查看多卷积核卷积计算流程:

1755690015755

可以看到:

  • 两个神经元,意味着有两个滤波器
  • 数据窗口每次移动两个步长取3*3的局部数据,即stride=2
  • zero-padding=1。输入数据由5*5*3变为7*7*3
  • 左边是输入(7*7*3中,7*7代表图像的像素/长宽,3代表R、G、B 三个颜色通道)
  • 中间部分是两个不同的滤波器Filter w0、Filter w1
  • 最右边则是两个不同的输出

3.6 特征图大小

输出特征图的大小与以下参数息息相关:

  1. size: 卷积核/过滤器大小,一般会选择为奇数,比如有 1*1, 3*35*5
  2. Padding: 零填充的方式
  3. Stride: 步长

那计算方法如下图所示:

  1. 输入图像大小: W x W
  2. 卷积核大小: F x F
  3. Stride: S
  4. Padding: P
  5. 输出图像大小: N x N

1755690031178

以下图为例:

  1. 图像大小: 5 x 5
  2. 卷积核大小: 3 x 3
  3. Stride: 1
  4. Padding: 1
  5. (5 - 3 + 2) / 1 + 1 = 5(如果除不尽向下取整), 即得到的特征图大小为: 5 x 5

1755690038311

3.7 PyTorch卷积层API

在PyTorch中进行卷积的API是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
conv = nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding)

"""
参数说明:
in_channels: 输入通道数,RGB图片一般是3
out_channels: 输出通道,也可以理解为卷积核kernel的数量
kernel_size:卷积核的高和宽设置,一般为3,5,7...
stride:卷积核移动的步长
整数stride:表示在所有维度上使用相同的步长 stride=2 表示在水平和垂直方向上每次移动2个像素
元组stride: 允许在不同维度上设置不同的步长 stride=(2, 1) 表示在水平方向上步长为2,在垂直方向上步长为1
padding:在四周加入padding的数量,默认补0
padding=0:不进行填充。
padding=1:在每个维度上填充 1 个像素(常用于保持输出尺寸与输入相同 padding=输入形状大小-输出形状大小)。
padding='same'(从 PyTorch 1.9+ 开始支持):让输出特征图的尺寸与输入保持一致。PyTorch会自动计算需要的填充量。stride必须等于1,不支持跨行,因为计算padding时可能出现小数
padding=kernel_size-1:Full Padding 完全填充
"""

我们接下来对下面的图片进行特征提取:

1755690048786

下面演示多通道多卷积核卷积:

1
2
3
4
5
6
7
8
9
10
11
12
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
import torch
import torch.nn as nn
import matplotlib.pyplot as plt

# 对图像进行卷积
# 1 读取图像 显示图像
# 2 定义卷积层
# 3 变换数据形状 1-转成tensor 2-通道要求[C H W] 3-批次数要求 [batch, C, H, W]
# 4 给卷积层喂数据 [1, 3, 640, 640] ---> [1, 4, 319, 319])
# (H-F+2p)/s +1 = (640-3+0)/2 + 1 = 319.5向下取整319
def test01():

# 1 读取图像
img = plt.imread('./data/img.jpg')
print('img.shape', img.shape)

plt.imshow(img)
plt.show()

# 2 定义卷积层
myconv2d = nn.Conv2d(in_channels=3, out_channels=4, kernel_size=3, stride=2, padding=0)
print('myconv2d--->', myconv2d)

# 3 变换数据形状
# ①转换成tensor
# ②通道要求[C, H, W], 默认[H, W, C]
# ③批次数要求[batch, C, H, W],多少个图像(一个图像是三维数组,四维有多少个三维就是多少个图像)
# [0, 1, 2] --> [2, 0, 1]
img2 = torch.tensor(img).permute(2, 0, 1)
print('img2.shape--->', img2.shape)
# 图像数为1,变为4维张量
img3 = img2.unsqueeze(0)
print('img3.shape--->', img3.shape)

# 4 给卷积层喂数据 [1, 3, 640, 640] ---> [1, 4, 319, 319])
# (H-F+2p)/s + 1 = (640-3+0)/2 + 1 = 319.5向下取整319
img4 = myconv2d(img3.type(torch.float32))
print('img4-->', img4.shape)


if __name__ == '__main__':
test01()

输出结果:

1
2
3
4
5
img.shape---> (640, 640, 3)
myconv2d---> Conv2d(3, 4, kernel_size=(3, 3), stride=(2, 2))
img2.shape---> torch.Size([3, 640, 640])
img3.shape---> torch.Size([1, 3, 640, 640])
img4--> torch.Size([1, 4, 319, 319])

对生成的特征图进行显示:

1
2
3
4
5
6
7
8
9
10
11
12
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
# 对图像卷积 并显示卷积以后的特征图
# 思路:去掉批次数 -> 转成[HWC] -->按照通道拿数据[:,:,0123]
def test02():

# 1 读取图像
img = plt.imread('./data/img.jpg')
print('img.shape', img.shape)

plt.imshow(img)
plt.show()

# 2 定义卷积层
myconv2d = nn.Conv2d(in_channels=3, out_channels=4, kernel_size=3, stride=2, padding=0)
print('myconv2d--->', myconv2d)

# 3 变换数据形状 1-转成tensor 2-通道要求[C H W] 3-批次数要求 [batch, C, H, W]
# [0, 1, 2] --> [2, 0, 1]
img2 = torch.tensor(img).permute(2, 0, 1)
print('img2.shape--->', img2.shape)

img3 = img2.unsqueeze(0)
print('img3.shape--->', img3.shape)

# 4 给卷积层喂数据 [1, 3, 640, 640] ---> [1, 4, 319, 319])
# (H-F+2p)/s +1 = (640-3+0)/2 + 1 = 319.5向下取整319
img4 = myconv2d(img3.type(torch.float32))
print('img4-->', img4.shape)

# 5 查看特征图思路:去掉批次数 -> 转成[HWC] -->按照通道拿数据[:,:,0123]
img5 = img4[0] # 去掉批次数
print('去掉批次数img5.shape-->', img5.shape)
img6 = img5.permute(1, 2, 0)
print('去掉批次数以后,再进行HWC img6.shape-->', img6.shape)

feature1 = img6[:, :, 0].detach().numpy()
feature2 = img6[:, :, 1].detach().numpy()
feature3 = img6[:, :, 2].detach().numpy()
feature4 = img6[:, :, 3].detach().numpy()

plt.imshow(feature1)
plt.show()

plt.imshow(feature2)
plt.show()

plt.imshow(feature3)
plt.show()

plt.imshow(feature4)
plt.show()


if __name__ == '__main__':
test02()

生成特征图显示:

1755690060555

4 池化层

池化层(Pooling Layer)是用于降低输入数据的空间维度(例如图像的高度和宽度),从而减少计算量、减少内存消耗,并提高模型的鲁棒性。

池化层通常位于卷积层之后,它通过对卷积层输出的特征图进行下采样,保留最重要的特征信息,同时丢弃一些不重要的细节。

池化层的主要作用如下:

  • 降维和计算量减少:池化层通过减少特征图的尺寸,从而降低了计算量,特别是在多层网络中,随着层数的增加,池化能够显著减少计算资源的消耗。

  • 提高鲁棒性:池化操作可以使得特征对小的变换、平移和旋转变得更加不敏感。这样,模型在面对噪声或图像的轻微变化时,依然能够稳定工作。

  • 防止过拟合:通过池化减少了特征图的大小,减少了模型的复杂度,从而有助于防止过拟合,尤其是在较小的数据集上。

  • 抽象特征:通过池化层的操作,可以提取更为抽象和高层次的特征,使得网络能够学习到更具泛化能力的表示。

4.1 池化层计算

  • 最大池化(Max Pooling) :通过池化窗口进行最大池化,取窗口中的最大值作为输出

    1755690081211

  • 平均池化(Avg Pooling) :取窗口内的所有值的均值作为输出

    1755690089866

4.2 Padding(填充)

1755690102727

4.3 Stride(步长)

1755690113060

4.4 多通道池化计算

在处理多通道输入数据时,池化层对每个输入通道分别池化,而不是像卷积层那样将各个通道的输入相加。这意味着==池化层的输出和输入的通道数是相等。==

池化只在宽高维度上池化在通道上是不发生池化(池化前后,多少个通道还是多少个通道)

1755690125462

3.5 PyTorch池化层API

在PyTorch中进行池化的API是:

1
2
3
4
5
6
7
8
9
10
# 最大池化
nn.MaxPool2d(kernel_size=2, stride=2, padding=1)
# 平均池化
nn.AvgPool2d(kernel_size=2, stride=1, padding=0)
"""
参数说明:
kernel_size:核的高和宽设置,一般为3,5,7...
stride:核移动的步长
padding:在四周加入padding的数量,默认补0
"""
  • 单通道池化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    import torch
    import torch.nn as nn


    # 1. 单通道池化
    # 定义输入数据 [1,3,3]
    inputs = torch.tensor([[[0, 1, 2], [3, 4, 5], [6, 7, 8]]], dtype=torch.float)
    # 修改stride,padding观察效果
    # 1. 最大池化
    pooling = nn.MaxPool2d(kernel_size=2, stride=1, padding=0)
    output = pooling(inputs)
    print("最大池化:\n", output)
    # 2. 平均池化
    pooling = nn.AvgPool2d(kernel_size=2, stride=1, padding=0)
    output = pooling(inputs)
    print("平均池化:\n", output)

    输出结果:

    1
    2
    3
    4
    5
    6
    最大池化:
    tensor([[[4., 5.],
    [7., 8.]]])
    平均池化:
    tensor([[[2., 3.],
    [5., 6.]]])
  • 多通道池化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 2. 多通道池化
    # 定义输入数据 [3,3,3]
    inputs = torch.tensor([[[0, 1, 2], [3, 4, 5], [6, 7, 8]],
    [[10, 20, 30], [40, 50, 60], [70, 80, 90]],
    [[11, 22, 33], [44, 55, 66], [77, 88, 99]]], dtype=torch.float)
    # 最大池化
    pooling = nn.MaxPool2d(kernel_size=2, stride=1, padding=0)
    output = pooling(inputs)
    print("多通道池化:\n", output)

    输出结果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    多通道池化:
    tensor([[[ 4., 5.],
    [ 7., 8.]],

    [[50., 60.],
    [80., 90.]],

    [[55., 66.],
    [88., 99.]]])

5 图像分类案例

咱们使用前面学习到的知识来构建一个卷积神经网络, 并训练该网络实现图像分类。要完成这个案例,咱们需要学习的内容如下:
了解 CIFAR10 数据集
搭建卷积神经网络
编写训练函数
编写预测函数

导入工具包

1
2
3
4
5
6
7
8
9
10
11
12
import torch
import torch.nn as nn
from torchvision.datasets import CIFAR10
from torchvision.transforms import ToTensor # pip install torchvision -i https://mirrors.aliyun.com/pypi/simple/
import torch.optim as optim
from torch.utils.data import DataLoader
import time
import matplotlib.pyplot as plt
from torchsummary import summary

# 每批次样本数
BATCH_SIZE = 8

5.1 CIFAR10 数据集

CIFAR-10数据集5万张训练图像、1万张测试图像、10个类别、每个类别有6k个图像,图像大小32×32×3。下图列举了10个类,每一类随机展示了10张图片:

1755690139198

PyTorch 中的 torchvision.datasets 计算机视觉模块封装了 CIFAR10 数据集, 使用方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 1. 数据集基本信息
def create_dataset():
# 加载数据集:训练集数据和测试数据
# ToTensor: 将image(一个PIL.Image对象)转换为一个Tensor
train = CIFAR10(root='data', train=True, transform=ToTensor())
valid = CIFAR10(root='data', train=False, transform=ToTensor())
# 返回数据集结果
return train, valid


if __name__ == '__main__':
# 数据集加载
train_dataset, valid_dataset = create_dataset()
# 数据集类别
print("数据集类别:", train_dataset.class_to_idx)
# 数据集中的图像数据
print("训练集数据集:", train_dataset.data.shape)
print("测试集数据集:", valid_dataset.data.shape)
# 图像展示
plt.figure(figsize=(2, 2))
plt.imshow(train_dataset.data[1])
plt.title(train_dataset.targets[1])
plt.show()

输出结果:

1
2
3
数据集类别: {'airplane': 0, 'automobile': 1, 'bird': 2, 'cat': 3, 'deer': 4, 'dog': 5, 'frog': 6, 'horse': 7, 'ship': 8, 'truck': 9} 
训练集数据集: (50000, 32, 32, 3)
测试集数据集: (10000, 32, 32, 3)

1755690154879

5.2 搭建图像分类网络

搭建的CNN网络结构如下:

1755690165308

我们要搭建的网络结构如下:

  1. 输入形状: 32x32
  2. 第一个卷积层输入 3 个 Channel, 输出 6 个 Channel, Kernel Size 为: 3x3
  3. 第一个池化层输入 30x30, 输出 15x15, Kernel Size 为: 2x2, Stride 为: 2
  4. 第二个卷积层输入 6 个 Channel, 输出 16 个 Channel, Kernel Size 为 3x3
  5. 第二个池化层输入 13x13, 输出 6x6, Kernel Size 为: 2x2, Stride 为: 2
  6. 第一个全连接层输入 576 维, 输出 120 维
  7. 第二个全连接层输入 120 维, 输出 84 维
  8. 最后的输出层输入 84 维, 输出 10 维

我们在每个卷积计算之后应用 relu 激活函数来给网络增加非线性因素。

构建网络代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
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
# 模型构建
class ImageClassification(nn.Module):
# 定义网络结构
def __init__(self):
super(ImageClassification, self).__init__()
# 定义网络层:卷积层+池化层
# 第一个卷积层, 输入图像为3通道,输出特征图为6通道,卷积核3*3
self.conv1 = nn.Conv2d(3, 6, stride=1, kernel_size=3)
# 第一个池化层, 核宽高2*2
self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)
# 第二个卷积层, 输入图像为6通道,输出特征图为16通道,卷积核3*3
self.conv2 = nn.Conv2d(6, 16, stride=1, kernel_size=3)
# 第二个池化层, 核宽高2*2
self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2)
# 全连接层
# 第一个隐藏层 输入特征576个(一张图像为16*6*6), 输出特征120个
self.linear1 = nn.Linear(576, 120)
# 第二个隐藏层
self.linear2 = nn.Linear(120, 84)
# 输出层
self.out = nn.Linear(84, 10)

# 定义前向传播
def forward(self, x):
# 卷积+relu+池化
x = torch.relu(self.conv1(x))
x = self.pool1(x)
# 卷积+relu+池化
x = torch.relu(self.conv2(x))
x = self.pool2(x)
# 将特征图做成以为向量的形式:相当于特征向量 全连接层只能接收二维数据集
# 由于最后一个批次可能不够8,所以需要根据批次数量来改变形状
# x[8, 16, 6, 6] --> [8, 576] -->8个样本,576个特征
# x.size(0): 第1个值是样本数 行数
# -1:第2个值由原始x剩余3个维度值相乘计算得到 列数(特征个数)
x = x.reshape(x.size(0), -1)
# 全连接层
x = torch.relu(self.linear1(x))
x = torch.relu(self.linear2(x))
# 返回输出结果
return self.out(x)


if __name__ == '__main__':
# 模型实例化
model = ImageClassification()
summary(model, input_size=(3,32,32), batch_size=1)

1755690179575

5.3 编写训练函数

在训练时,使用多分类交叉熵损失函数,Adam 优化器。具体实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
def train(model, train_dataset):
# 构建数据加载器
dataloader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True)
criterion = nn.CrossEntropyLoss() # 构建损失函数
optimizer = optim.Adam(model.parameters(), lr=1e-3) # 构建优化方法
epoch = 100 # 训练轮数
for epoch_idx in range(epoch):
sum_num = 0 # 样本数量
total_loss = 0.0 # 损失总和
correct = 0 # 预测正确样本数
start = time.time() # 开始时间
# 遍历数据进行网络训练
for x, y in dataloader:
model.train()
output = model(x)
loss = criterion(output, y) # 计算损失
optimizer.zero_grad() # 梯度清零
loss.backward() # 反向传播
optimizer.step() # 参数更新
correct += (torch.argmax(output, dim=-1) == y).sum() # 计算预测正确样本数
# 计算每次训练模型的总损失值 loss是每批样本平均损失值
total_loss += loss.item()*len(y) # 统计损失和
sum_num += len(y)
print('epoch:%2s loss:%.5f acc:%.2f time:%.2fs' %(epoch_idx + 1,total_loss / sum_num,correct / sum_num,time.time() - start))
# 模型保存
torch.save(model.state_dict(), 'model/image_classification.pth')


if __name__ == '__main__':
# 数据集加载
train_dataset, valid_dataset = create_dataset()
# 模型实例化
model = ImageClassification()
# 模型训练
train(model,train_dataset)

输出结果:

1
2
3
4
5
6
7
8
9
10
11
epoch: 1 loss:1.59926 acc:0.41 time:28.97s
epoch: 2 loss:1.32861 acc:0.52 time:29.98s
epoch: 3 loss:1.22957 acc:0.56 time:29.44s
epoch: 4 loss:1.15541 acc:0.59 time:30.45s
epoch: 5 loss:1.09832 acc:0.61 time:29.69s
...
epoch:96 loss:0.30592 acc:0.89 time:37.28s
epoch:97 loss:0.29255 acc:0.90 time:37.11s
epoch:98 loss:0.29470 acc:0.90 time:36.98s
epoch:99 loss:0.29472 acc:0.90 time:36.79s
epoch:100 loss:0.29903 acc:0.90 time:37.66s

5.4 编写预测函数

加载训练好的模型,对测试集中的1万条样本进行预测,查看模型在测试集上的准确率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def test(valid_dataset):
# 构建数据加载器
dataloader = DataLoader(valid_dataset, batch_size=BATCH_SIZE, shuffle=False)
# 加载模型并加载训练好的权重
model = ImageClassification()
model.load_state_dict(torch.load('model/image_classification.pth'))
# 模型切换评估模式, 如果网络模型中有dropout/BN等层, 评估阶段不进行相应操作
model.eval()
# 计算精度
total_correct = 0
total_samples = 0
# 遍历每个batch的数据,获取预测结果,计算精度
for x, y in dataloader:
output = model(x)
total_correct += (torch.argmax(output, dim=-1) == y).sum()
total_samples += len(y)
# 打印精度
print('Acc: %.2f' % (total_correct / total_samples))


if __name__ == '__main__':
test(valid_dataset)

输出结果:

1
Acc: 0.57

5.5 模型优化

CNN网络模型在训练集样本上的准确率远远高于测试集,说明模型产生了过拟合问题,我们把学习率由1e-3修改为1e-4、增加网络参数量和增加dropout正则化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class ImageClassification(nn.Module):
def __init__(self):
super(ImageClassification, self).__init__()
self.conv1 = nn.Conv2d(3, 32, stride=1, kernel_size=3)
self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)
self.conv2 = nn.Conv2d(32, 128, stride=1, kernel_size=3)
self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2)

self.linear1 = nn.Linear(128 * 6 * 6, 2048)
self.linear2 = nn.Linear(2048, 2048)
self.out = nn.Linear(2048, 10)
# Dropout层,p表示神经元被丢弃的概率
self.dropout = nn.Dropout(p=0.5)

def forward(self, x):
x = torch.relu(self.conv1(x))
x = self.pool1(x)
x = torch.relu(self.conv2(x))
x = self.pool2(x)
# 由于最后一个批次可能不够 32,所以需要根据批次数量来 flatten
x = x.reshape(x.size(0), -1)
x = torch.relu(self.linear1(x))
# dropout正则化
# 训练集准确率远远高于测试准确率,模型产生了过拟合
x = self.dropout(x)
x = torch.relu(self.linear2(x))
x = self.dropout(x)
return self.out(x)

经过训练,模型在测试集的准确率由 0.57,提升到了 0.93,同学们也可以自己修改相应的网络结构、训练参数等来提升模型的性能。

注意力机制

注意力机制

1 注意力机制由来

  • seq2seq架构介绍(encoder-decoder)

    • encoder:编码器, 生成固定上下文张量c
    • decoder:解码器, 生成预测序列
      • 自回归预测: 只能使用上一时间步的预测结果作为下一时间步的输入

    1748483329298

  • seq2seq架构存在问题

    • c不变->信息不变/信息瓶颈
    • 使用GRU模型, 处理超长序列时也会产生梯度消失或梯度爆炸
    • 基于以上两个问题引用了注意力机制

2 注意力机制介绍

  • 概念
    • 一种增强神经网络模型性能的技术/工具
    • 预测时每个时间步都要计算一个中间语义张量C(动态C) C1,C2,C3…
      • C1 = 0.5欢迎 + 0.3来 + 0.2北京
      • C2 = 0.3欢迎 + 0.6来 + 0.1北京
  • 核心思想
    • 通过计算==动态中间语义张量c==来增强模型表达能力
  • 作用
    • 增强神经网络模型性能
      • 增强可解释性 -> 权值
      • 缓解信息瓶颈问题 -> 动态C
      • 解决长序列问题 -> 使用自注意力机制替换RNN/LSTM/GRU 解决梯度消失或爆炸问题

3 注意力机制分类

3.1 soft attention(软注意力)

概念: 关注所有的输入词, 给每个词添加0-1之间的概率权重, 所有词的概率权重相加为1

  • 不加attention的seq2seq框架

    • 编码器计算==固定长度的中间语义张量c==
    • 解码器一步一步解码,每一个时间步使用 固定c和上一时间步预测值融合的结果->X 再和 上一时间步的隐藏状态值 得到当前时间步的预测结果
  • 加attention的seq2seq框架

    • 编码器计算每个时间步的隐藏状态值 h1 h2 h3 …
    • 计算解码器每个时间步隐藏状态值前权重系数(经过softmax处理) w1h1 w2h2 w3h3…
      • h1 h2 h3 拼接结果就是初始的中间语义张量c
    • 每个时间步的w1h1进行加权求和计算出动态中间语义张量c
  • 注意力权重(概率)计算 (a11 a12 a13的结果值)

    1748493123217

  • 注意力的三步计算流程

    • query和key进行相似度计算, 得到 attention score(权重分值)
      • 点积
      • 缩放点积
      • 加性
    • attention score经过softmax, 得到 attention prob(权重概率)
    • attention prob和value进行加权求和, 得到 动态中间语义张量c

    1748502589934

  • query、key、value含义

    • query: 解码器时间步的输入x/隐藏状态值
    • key: 编码器时间步的词语嵌入向量/隐藏状态值
    • value: 编码器时间步的词语嵌入向量/隐藏状态值
    • nlp中一般情况下, key=value
    • query!=key=value -> 一般注意力机制/软注意力机制
    • query=key=value -> 自注意力机制

3.2 hard attention(硬注意力)

概念: 关注部分的输入词, 非0即1, 关注1对应的词

  • 选择概率最大k个token,将对应权重设为1,其他的都是0
  • 随机采样, 选择k个token
  • 在强化学习中使用

3.3 self attention(自注意力)

概念: 舍弃传统的RNN/LSTM/GRU模型, 使用相似度计算的某种方式(缩放点积)算词之间的语义->并行计算, 信息不丢失

  • transformer编码器或解码器中 -> query=key=value
  • 并行计算两两token之间的相似度
    • 编码器端计算==上下文==两两token的相似度
    • 解码器端计算==上文==两两token的相似度

4 注意力机制规则

  • 加性注意力

    • 先将q和k根据特征维度进行拼接后进行线性计算 -> softmax -> 和V加权求和(三维矩阵乘法)

      1748512221326

    • 先将q和k根据特征维度进行拼接后进行线性计算[继续进行tanh激活计算,sum求和] -> softmax -> 和V加权求和(三维矩阵乘法)

      1748512227535

  • 点积注意力

    • q和k的转置进行点积 -> softmax -> 和V加权求和(三维矩阵乘法)
  • 缩放点积注意力

    • q和k的转置进行点积, 点积结果除以sqrt(dk) -> softmax -> 和V加权求和(三维矩阵乘法)

    • 为什么除以sqrt(dk)?

      • 防止点击结果值过大, 导致梯度饱和(全为1, 对称性问题)

      1748512378106

5 深度神经网络的注意力机制

6 seq2seq架构中注意力机制

  • 编码器端的注意力机制
    • 自注意力机制 query=key=value
      • 并行计算两两token之间的语义关系(捕获语义) -> 计算上下文的token
  • 解码器端的注意力机制
    • 自注意力机制 query=key=value
      • 并行计算两两token之间的语义关系(捕获语义) -> 计算上文的token
    • 一般注意力机制 query!=key=value
      • 让解码器选择性地关注编码器输出的相关信息

网络编程_进程_线程

网络编程介绍

  • 概述

    就是用来实现网络互联的 不同计算机上 运行的程序间 可以进行数据交互.

  • 三要素

    • IP地址: 设备(电脑, 手机, Ipad…)在网络中的唯一标识

      分类:

      ​ IPV4, 4字节, 十进制. 例如: 192.168.88.100

      ​ IPV6, 8字节, 十六进制, 宣传语: 可以让地球上的每一粒沙子都有自己的IP

      两个DOS命令:

      ​ 查看IP:

      ​ windows: ipconfig

      ​ Linux, Mac: ifconfig

      ​ 测试网络连接:

      ​ ping ip地址 或者 域名

    • 端口号: 程序在设备(电脑, 手机, Ipad…)上的唯一标识.

      范围: 0 ~ 65535, 其中0 ~ 1023已经被系统占用或者用作保留端口, 自定义端口时尽量规避这个范围.

    • 协议: 传输规则, 规范.

      常用的协议:

      ​ TCP(这个用的最多) 和 UDP

      TCP特点:

      ​ 1.面向有连接

      ​ 2.采用字节流传输数据, 理论无大小限制.

      ​ 3.安全(可靠)协议.

      ​ 4.效率相对较低.

      ​ 5.区分客户端和服务器端.

入门创建Socket对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
"""
网络编程介绍:
概述:
网络编程也叫网络同学,socket通信。即通信双方都有自己的socket对象
大白话:
你和你遥远的朋友在聊天的时候,看似你们两个在交互,其实通过手机(双方手机)来交互
"""

#1.导包
import socket

#2.创建对象
# AddressFamily :地址族 即 IPV4 还是用IPV6 默认AF_INET(IPV4) AF_INET6(IPV6)
# socket type :socket类型 即 TCP 还是 UDP 默认SOCK_STREAM (TCP) SOCK_DGRAM(UDP)
cr_socket=socket.socket(socket.AF_INET,socket.SOCK_STREAM) # IPV4 和 TCP
print(cr_socket)

网编案例_一句话交情

  • 图解

    1742178741721

  • 服务器端代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    """
    案例:网络编程入门 服务器给客户端发送消息,客户端给服务器回执信息
    1. 创建服务端套接字对象
    2. 绑定端口号
    3. 设置监听
    4. 等待接受客户端的连接请求
    5. 发送数据
    6. 接收数据
    7. 关闭套接字

    细节:客户端和服务器都是流程 字节流的形式实现的
    """
    # 0.导包
    import socket

    # 1. 创建服务端套接字对象
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 2. 绑定端口号 已元组形式进行传递数据ip 和端口号
    server_socket.bind(('127.0.0.1', 8888))
    # 3. 设置监听
    server_socket.listen(5)
    # 4. 等待接受客户端的连接请求 (拆包)
    accept_socket, client_info = server_socket.accept()
    # 5. 发送数据 b是把数据转换为二进制(字母、数字、特殊符号)
    accept_socket.send(b"welcome to socket")
    # 6. 接收数据
    data = accept_socket.recv(1024).decode("utf-8")
    print(f"服务器端收到来自{client_info}的信息:{data}")
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
    # 7. 关闭套接字
    accept_socket.close()
    # server_socket.close() #服务器端一般不关闭

  • 客户端代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    """
    TCP客户端操作步骤流程:
    1. 创建客户端套接字对象
    2. 客户端连接服务器端
    3. 接收数据
    4. 发送数据
    5. 关闭套接字
    """
    import socket
    # 1. 创建客户端套接字对象
    clinet_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 2. 客户端连接服务器端
    clinet_socket.connect(("127.0.0.1", 8888))
    # 3. 接收数据
    data = clinet_socket.recv(1024).decode("utf-8")
    print(f"客户端收到:{data}")
    # 4. 发送数据
    clinet_socket.send("socket很好玩,很有趣,我很喜欢!".encode("utf-8"))
    # 5. 关闭套接字
    clinet_socket.close()

网编案例_模拟多任务服务器端

1
2
3
4
5
6
7
8
9
10
11
12
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
"""
案例:网络编程入门 服务器给客户端发送消息,客户端给服务器回执信息
1. 创建服务端套接字对象
2. 绑定端口号
3. 设置监听
4. 等待接受客户端的连接请求
5. 发送数据
6. 接收数据
7. 关闭套接字

细节:客户端和服务器都是流程 字节流的形式实现的
循环实现接受多个客户端消息
"""
# 0.导包
import socket

# 1. 创建服务端套接字对象
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2. 绑定端口号 已元组形式进行传递数据ip 和端口号
server_socket.bind(('192.168.13.62', 8888))
# 3. 设置监听
server_socket.listen(5)
# 4. 等待接受客户端的连接请求 (拆包)
while True:
try:
accept_socket, client_info = server_socket.accept()
# 5. 发送数据 b是把数据转换为二进制(字母、数字、特殊符号)
accept_socket.send(b"welcome to socket")
# 6. 接收数据
data = accept_socket.recv(1024).decode("utf-8")
print(f"服务器端收到来自{client_info}的信息:{data}")
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
# 7. 关闭套接字
accept_socket.close()
# server_socket.close() #服务器端一般不关闭
except:
pass

扩展_编解码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
"""
细节:
编码 : 把我们看的懂得 转成 我们看不懂的
'字符串'.encode(码表)
解码: 把我们看不懂的 转成 我们看得懂
二进制.decode(码表)
乱码:编解码格式不一致
二进制数据的特殊写法:即 b 字母、数字、特殊符号
"""

s1 = '黑马1234abc!'
# 编码
print(s1.encode())
print(s1.encode("utf-8"))
# 结论 当没有指定码表的时候默认就是 utf-8

# 解码
s2 = b'\xe9\xbb\x91\xe9\xa9\xac1234abc!'
print(type(s2))
print(s2.decode()) # 默认解码格式utf-8

s4="黑马"
sgbk = s4.encode("gbk")
print(sgbk)
print(sgbk.decode("utf-8"))

网编案例_文件上传

  • 图解

    1742186023575

  • 服务器端代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    """
    1:创建socket对象
    2:绑定ip、端口号
    3:设置最大监听数
    4:等待客户端申请连接
    5:读取客户端上传的文件数据,写入到目的文件
    6:释放资源
    """
    import socket

    # 1:创建socket对象
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 2:绑定ip、端口号
    server_socket.bind(("127.0.0.1", 9999))
    # 3:设置最大监听数
    server_socket.listen(10)
    # 4:等待客户端申请连接
    accept_socket, clien_info = server_socket.accept()
    # 5:读取客户端上传的文件数据,写入到目的文件
    with open("./data/my.txt", "wb") as dats_f:
    # 5.1循环读取
    while True:
    # 5.2接受客户端上传数据
    data_bys = accept_socket.recv(1024)
    if len(data_bys) == 0:
    break
    dats_f.write(data_bys)
    # 6:释放资源
    accept_socket.send("文件上传成功!".encode("utf-8"))
    accept_socket.close()

  • 客户端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    """
    1:创建客户端socket对象
    2:连接服务器端 ip 和端口
    3:关联数据源文件 读取内容 ,写给服务器端
    4:释放资源
    """
    import socket
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client_socket.connect(("127.0.0.1", 9999))
    # 3:关联数据源文件 读取内容 ,写给服务器端
    with open("d:/绕口令.txt", "rb") as src_f:
    while True:
    data = src_f.read(1024)
    # 发送给服务器端
    client_socket.send(data)
    if len(data) == 0:
    break
    print("客户端收到消息")
    client_socket.close()

扩展_模拟多任务版文件上传服务器端

1
2
3
4
5
6
7
8
9
10
11
12
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
"""
1:创建socket对象
2:绑定ip、端口号
3:设置最大监听数
4:等待客户端申请连接
5:读取客户端上传的文件数据,写入到目的文件
6:释放资源
"""
import socket

# 1:创建socket对象
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2:绑定ip、端口号
server_socket.bind(("192.168.13.62", 9999))
# 3:设置最大监听数
server_socket.listen(10)
count = 0
while True:
count+=1
try:
# 4:等待客户端申请连接
accept_socket, clien_info = server_socket.accept()
# 5:读取客户端上传的文件数据,写入到目的文件
with open("./data/picture_"+str(count)+'.jpg', "wb") as dats_f:
# 5.1循环读取
while True:
# 5.2接受客户端上传数据
data_bys = accept_socket.recv(3072)
if len(data_bys) == 0:
break
dats_f.write(data_bys)
# 6:释放资源
accept_socket.send("文件上传成功!".encode("utf-8"))
accept_socket.close()
except:
pass

多任务简介

  • 概述

    让多个任务”同时”执行, 目的是: 充分利用CPU资源, 提高程序的执行效率.

  • 方式

    • 并发: 针对于单核CPU来讲, 多个任务同时请求执行时, CPU做高效切换即可.
    • 并行:针对于多核CPU来讲, 多个任务同时执行.
  • 图解

    1742195490886

单任务代码演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
"""
案例: 演示单任务, 前边不执行完毕, 后边绝对无法执行.
"""

# 1.定义函数A, 输出10次 hello world
def func_a():
for i in range(1000000):
print("hello world")

# 2. 定义函数B, 输出10次 hello python
def func_b():
for i in range(2):
print("hello python")


func_a()
print('-' * 23)
func_b()

多进程入门案例

  • 入门案例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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
    """
    多进程实现步骤:
    1. 导入进程工具包
    import multiprocessing
    2. 通过进程类 实例化进程 对象
    子进程对象 = multiprocessing.Process()
    3. 启动进程执行任务
    进程对象.start()
    需求:使用多进程来模拟一边编写代码,一边听音乐功能实现。
    """
    # 1. 导入进程工具包
    import multiprocessing
    import time

    # 编写代码
    def coding():
    for i in range(1, 20):
    time.sleep(0.1)
    print(f"正在敲第{i}遍代码")
    # 听音乐
    def music():
    for i in range(1, 20):
    time.sleep(0.1)
    print(f"正在听第{i}遍音乐")

    if __name__ == '__main__':
    #单任务
    # music()
    # coding()

    # 2.通过进程类实例化进程对象
    p1 = multiprocessing.Process(target=coding)
    p2 = multiprocessing.Process(target=music)

    p1.start()
    p2.start()

  • 带参数的多进程代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    """
    使用多进程来模拟小明一边编写num行代码,一边听count首音乐功能实现。

    进程传参的方式有两种
    1:args方式 接受所有参数,以元组的方式 (位置参数)
    2:kwargs方式 接受所有参数,以字典的方式 (关键字参数)
    """
    # 1. 导入进程工具包
    import multiprocessing
    import time


    # 编写代码
    def coding(name,age):
    for i in range(1, 20):
    print(name, age,end="")
    time.sleep(0.1)
    print(f"正在敲第{i}遍代码")


    # 听音乐
    def music(name,age):
    for i in range(1, 20):
    print(name, age,end="")
    time.sleep(0.1)
    print(f"正在听第{i}遍音乐")


    if __name__ == '__main__':
    # 创建两个子进程 ,并且给进程传递参数
    p1 = multiprocessing.Process(target=coding, name="Process-1",args=('小明', 30))
    p2 = multiprocessing.Process(target=music, name="Process-2",kwargs={'name': '某只羊', "age": 33})
    p1.name


获取进程编号

1
2
3
4
5
6
7
8
9
10
11
12
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
56
"""
进程编号解释:
概述:
在设备中,每个程序(进程)都有用自己的唯一的进程id,当程序释放的时候,该进程id也会释放,即
进程id可以重复使用
目的:
1:查看子进程的父进程 方便管理
2:例如杀死指定进程
格式:
方式一:os.getpid()的使用
方式二:import multiprocessing
pid = multiprocessing.current_process().pid

细节: main中创建进程,如果没有特殊指定,他的父进行都是main进程
而main进程的父进程,pycham程序是pid

"""
import os

"""
使用多进程来模拟小明一边编写num行代码,一边听count首音乐功能实现。

进程传参的方式有两种
1:args方式 接受所有参数,以元组的方式 (位置参数)
2:kwargs方式 接受所有参数,以字典的方式 (关键字参数)
"""
# 1. 导入进程工具包
import multiprocessing
import time

# 编写代码
def coding(name, age):
for i in range(1, 20):
print(name, age, end="")
time.sleep(1)
print(f"正在敲第{i}遍代码")
print(f"p1进程的pid{os.getpid()},{multiprocessing.current_process().pid},父进程id(ppid):{os.getppid()}")


# 听音乐
def music(name, age):
for i in range(1, 20):
print(name, age, end="")
time.sleep(1)
print(f"正在听第{i}遍音乐")
print(f"p2进程的pid{os.getpid()},{multiprocessing.current_process().pid},父进程id(ppid):{os.getppid()}")

if __name__ == '__main__':
# 创建两个子进程 ,并且给进程传递参数
p1 = multiprocessing.Process(target=coding, name="Process-1", args=('小明', 30))
p2 = multiprocessing.Process(target=music, name="Process-2", kwargs={'name': '某只羊', "age": 33})
p1.start()
p2.start()
# 查看主进程信息
print(f"main进程的pid{os.getpid()},{multiprocessing.current_process().pid},父进程id(ppid):{os.getppid()}")

进程特点_数据隔离

  • 图解

    1742899626719

  • 代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    """
    前置要求: (了解代码看懂现象)
    进程特点:
    1:进程之间的数据是相互隔离的。
    因为子进程相当于父进程的“副本”。会将父进行的main外资源拷贝一份。即:各是各的
    2:默认情况下,主进程会等待子进程执行结束再结束
    """
    import multiprocessing
    import time

    # 需求:在不同进程中修改列表my_list[]并新增元素,试着在各个进程中观察列表的最终结果

    # 1、定义一个公共的数据容器my_list[]
    my_list = []


    # 2、定义函数,向容器中添加数据
    def writer_data():
    for i in range(1, 6):
    my_list.append(i)
    print(f"添加数据:{i}")
    # print(f"writer_data函数{my_list}")
    # 定义函数 ,从容器中读取数据
    def read_data():
    time.sleep(3)
    print(f"read_data函数:{my_list}")


    if __name__ == '__main__':
    p1 = multiprocessing.Process(target=writer_data)
    p2 = multiprocessing.Process(target=read_data)

    p1.start()
    p2.start()

进程特点_守护进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
"""
进程特点:
1:进程之间的数据是相互隔离的。
因为子进程相当于父进程的“副本”。会将父进行的main外资源拷贝一份。即:各是各的
2:默认情况下,主进程会等待子进程执行结束再结束
思路1:设置子进程为“守护”线程。
思路2:强制关闭子进程。可能会导致子进程变为僵尸进程。交由python解释器自动回收(底层有init初始化进程来进行严格管理)
"""
import multiprocessing
import time


def work():
for i in range(100):
print("我努力工作中.....")
time.sleep(0.2)


if __name__ == '__main__':
# p1进程
p1 = multiprocessing.Process(target=work)
# 设置守护进程 为p1设置守护进程
# p1.daemon = True

p1.start()

# 主进程(main)休眠1s后结束
time.sleep(1)
#思路2:强制关闭子进程
p1.terminate()
print("main进程结束了")

线程入门

  • 无参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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
    """

    线程使用步骤
    1:导包
    2:创建线程对象
    3:启动线程

    线程和进程的关系
    1:进程是CPU分配资源的基本单位,线程是CPU调度资源的最小单位
    2:线程依附与进程的。每个进程至少有一个线程(主线程)
    3:进程间数据是相互隔离的。(同一个进程)线程间的数据是可以共享的
    """
    import threading, time


    # 编写代码
    def coding():
    for i in range(1, 200):
    time.sleep(0.1)
    print(f"正在敲第{i}遍代码")


    # 听音乐
    def music():
    for i in range(1, 200):
    time.sleep(0.1)
    print(f"正在听第{i}遍音乐")


    if __name__ == '__main__':
    t1 = threading.Thread(target=coding)
    t2 = threading.Thread(target=music)

    #这里 A

    t1.start()
    t2.start()

    #这里 B
    for i in range(1,20):
    time.sleep(0.1)
    print(f"main{i}")

数据结构与算法1

数据结构和算法简介

  • 数据结构

    就是存储和组织数据的方式, 分为: 线性结构非线性结构

  • 算法

    就是解决问题的思路和发放, 它具有独立性, 即: 它不依赖语言, 而是解决问题的思路. Java能做, Python也能做.

    • 特性

      有输入, 有输出, 有穷性, 确定性, 可行性.

    • 如何衡量算法的优劣

      ==大O标记法,== 即: 将次要条件都省略掉, 最终形成1个表达式.

      **主要条件:**随着问题规模变化而==变化==的.

      **次要条件:**随则问题规模变化而==不变==的.

      1742438995269

    • 最优和最坏时间复杂度

      如非特殊说明, 我们考虑的都是 最坏时间复杂度, 因为它是算法的一种保证.

      而最优时间复杂度是算法的 最理想, 最乐观的状况, 没有太大的参考价值.

    • 常见的时间复杂度如下

      从最优到最坏分别是:

      O(1) -> O(logn) -> O(n) -> O(n logn) -> O(n²) -> O(n³)

      常数阶 -> 对数阶 -> 线性阶 -> 线性对数阶 -> 平方阶 -> 立方阶

    • 常见的空间复杂度如下

      了解即可, 因为服务器(内存)资源一般是足够的

      从最优到最坏分别是:

      O(1) -> O(logn) -> O(n) -> O(n²) -> O(n³)

数据结构分类

  • 分类

    数据结构 = 存储, 组织数据的方式, 是算法解决问题时的载体.

    • 线性结构

      特点: 每个节点都只能有1个前驱, 1个后继节点.

      例如: 顺序表(栈, 队列), 链表

    • 非线性结构

      特点: 每个节点都可以有多个前驱, 多个后继节点.

      例如: 树, 图

线性结构存储数据的方式

  • 图解

    1742449680820

  • 顺序表存储方式详解

    • 一体式存储

      1742449752895

    • 分离式存储

      1742449775956

顺序表的存储方式

  • 解释

    顺序表有 数据区 和 信息区两部分组成.

  • 特点

    • 数据区 和 信息区在一起的 -> 一体式存储(扩容时只能整体搬迁)
    • 数据取货 和 信息区分开存储的 -> 分离式存储(可以直接让信息区指向新的 数据区即可, 不用整体搬迁).
  • 顺序表扩容策略

    思路1: 每次增加固定的容量. 拿时间换空间

    思路2: 每次扩容, 容量翻倍. 拿空间换时间

线性结构之链表介绍

  • 概述

    链表属于 线性结构, 在存储时 不要求连续的内存空间, 只要有地儿就行.
    可以简单理解为, 它是用来解决顺序表的弊端的(必须要有足够的连续空间, 否则扩容失败.)

  • 组成

    链表 由 节点 组成. 节点又分为 数值域(元素域) 和 地址域(链接域), 根据节点不同, 链表可以分为 四大类.

  • 图解

    1743163476385

    1743163490026

    1743163501025

    自定义代码模拟链表

1
2
3
4
5
6
7
8
9
10
11
12
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
56
57
58
59
60
61
62
63
64
65
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
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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
"""
1:链表介绍
概述:
他是数据结果的线性结构的一种,每一个阶段只有一个前驱和一个后继
作用:
优化顺序表的弊端(如果没有足够的内存空间,会导致扩容失败)
链表的扩容,有地就行。连不连续无所谓
组成:
链表 有 节点 组成 ,节点由 元素域 和链接域 组成
分类:
单链表
单循环链表
双链表
双循环链表

自定义代码模拟链表 思路分析:
1:自定义SingleNode类 表示节点类
属性:
item 数值域(元素域)
next 地址域(链接域) 不是他的地址,而是他的下一个节点地址
2:自定义SingleLinkedList类 表示:链表
属性:
head 表示头结点 ,指向链表的第一个节点
行为:
is_empty(self) :链表是否为空
length(self):判断链表长度
traverse(self):遍历整个链表
add(self,item) :给链表头部添加元素
append(self,item):链表尾部添加元素
insert(self,item):指定位置添加元素
remove(self,item):删除节点
search(self,item):查询节点是否存在
"""


# 自定义SingleNode类,表示节点类
class SingleNode:
# 初始化属性
def __init__(self, item):
self.item = item # 元素域(数值域)
self.next = None # 链接域(地址域)


# 自定义SingleLinkList类,表示链表
class SingleLinkList:
def __init__(self, node=None):
self.head = node

# is_empty 链表是否为空
def is_empty(self):
# 思路:判断头结点是否为None,如果为None,则链表为空
# 写法一 if else
# if self.head is None:
# return True
# else:
# return False
# 写法二
# return True if self.head is None else False
# 写法三(推荐)
return self.head is None

def length(self): # 判断链表长度
# 默认从头开始
cur = self.head
# 初始化计数器
count = 0
# 开始遍历 ,只要当前节点不为空,就一直循环
while cur is not None:
count += 1
cur = cur.next
return count

def traverse(self): # 遍历整个链表
# 找到默认开头节点
cur = self.head
# 定义循环判断
while cur is not None:
print(f"数值域:{cur.item}", end="")
cur = cur.next

def add(self, item): # 给链表头部添加元素
# 1、创建一个新的节点
new_node = SingleNode(item)
# 设置新节点的地址域,指向头节点
new_node.next = self.head
# 设置头节点指向新节点
self.head = new_node

def append(self, item): # 链表尾部添加元素
new_node = SingleNode(item)
if self.is_empty():
self.head = new_node
else:
# 走到这里,说明链表不为空,需要找到链表的尾节点
# 默认从头开始
cur = self.head
while cur.next is not None:
# 地址后移
cur = cur.next
# 走到这里,cur就是最后一个节点,所以我们设置它的地址指向新的节点
cur.next = new_node

def insert(self, pos, item): # 指定位置添加元素
# 头插
if pos <= 0:
self.add(item)
# 尾插
elif pos >= self.length():
self.append(item)
else: # 任意位置
# 走到这里,说明pos给合法的,即:中间值,想到找到插入到哪个元素
cur = self.head
# 定义当前节点的位置
count = 0
while count < pos - 1:
# 走到这里,说明还没有找到插入前的那个节点,就节点后移,计数器+1
cur = cur.next
count += 1
# 走到这里,cur就是插入位置前的那个节点,封装新节点
new_node = SingleNode(item)
# 设置新节点的地址域,指向插入位置前的那个节点的地址域
new_node.next = cur.next
# 设置插入位置前的那个节点的地址域指向新节点
cur.next = new_node

def remove(self, item): # 删除节点
# 默认从头节点开始
cur = self.head
pre = None
while cur is not None:
# 判断当前节点是否是要删除的节点
if cur.item == item:
# 判断要删除的节点是否是头节点
if cur == self.head:
# 直接设置头结点为当前节点的下一个节点
self.head = cur.next
else:
# 走到这里,说明要删除的节点不是头节点,直接设置前驱(pre)的地址域指向当前节点的地址域即可
pre.next = cur.next
cur.next = None
# 走到这里,说明删除成功,返回即可
return
else:
# 走到这里,说明当前节点不是要删除的节点,后移,pre也要后移
pre = cur
cur = cur.next

def search(self, item): # 查询节点是否存在
# 默认从头节点开始
cur = self.head
# 只要当前节点不为空,就一直循环
while cur is not None:
if cur.item == item:
return True
# 如果当前节点不是要找的节点,后移
cur = cur.next
# 走到这里,所以节点就都找完了,还没有找到 ,返回False
return False


if __name__ == '__main__':
# 1.1测试节点类
node1 = SingleNode(10)
# 1.2 打印当前节点的元素域 和链接域
print(f"元素域(数值域):{node1.item}")
print(f"链接域(地址域):{node1.next}")
print(f"node1对象{node1}")
print(f"node1类型{type(node1)}")
print("---------------测试链接类-------------------")
# 2.1 测试链接类
my_linklist = SingleLinkList(node1)
print(f"头结点:{my_linklist}")
print(f"头节点的元素域{my_linklist.head.item}") # 10
print(f"头节点的地址域{my_linklist.head.next}") # None
print("---------------测试链表是否为空-------------------")
# 3.1测试链表是否为空
print(my_linklist.is_empty())
print("---------------测试头插-------------------")
my_linklist.add(20)
my_linklist.add(30)
print("---------------测试尾插-------------------")
my_linklist.append(40)
my_linklist.append(50)
print("---------------测试任意位置插入-------------------")

my_linklist.insert(-1, 2)
my_linklist.insert(8, 55)
my_linklist.insert(2, 32)

print("---------------测试删除-------------------")
my_linklist.remove(2)
my_linklist.remove(30)
my_linklist.remove(60)
print("---------------测试查找-------------------")
print(my_linklist.search(2))
print(my_linklist.search(32))

print("---------------测试链表长度-------------------")
print(f"当前链表长度为:{my_linklist.length()}")
print("---------------测试遍历-------------------")
my_linklist.traverse()

数据结构与算法

冒泡排序_思路及代码

1743382426395

1
2
3
4
5
6
7
8
9
10
11
12
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
"""
排序算法稳定性介绍:
概述:排序算法= 把一串数据按照升序或者降序的方式进行排列 方法、思维、方式
分类:
稳定排序算法
排序后,想同元素的相对位置不发生改变
不稳定排序算法
排序后,想同元素的相对位置发生改变
冒泡排序:
原理:相邻元素两两比较,大的往后走,这样第一轮比较厚,最大值在最大索引处
重复该动作,只要排序完毕。
核心:
1:比较的总轮数 列表的长度-1
2:每轮比较的总次数 列表的长度-1-当前轮数的索引
3:谁和谁比较 ? 列表[j]和列表[j+1]
分析流程: 假设元素个数5个 ,具体如下: [5,3,4,7,2] 长度为5
比较的轮数,i 每轮比较的次数,j
第一轮:索引0 4->5-1-0
第二轮:索引1 3->5-1-1
第三轮:索引2 2->5-1-2
第四轮:索引3 1->5-1-3
总结:
冒泡排序属于 稳定
最优的时间复杂度多少 ? o(n)
最坏的时间复杂度多少?o(n²)

"""
# 冒泡排序函数
def bubble_sort(my_list):
# 1、获取列表的长度 n代表列表长度
n = len(my_list)
# 定义循环, 外循环控制比较的轮数 内循环控制比较次数
for i in range(n - 1): # i 0 1 2 3
for j in range(n - 1 - i):
# 具体的比较动作:相邻的两两元素比较,大的往后
if my_list[j] > my_list[j + 1]:
my_list[j], my_list[j + 1] = my_list[j + 1], my_list[j]

my_list=[5,3,4,7,2]
print(f"排序前:{my_list}")
bubble_sort(my_list)
print(f"排序后:{my_list}")

选择排序_思路及代码

  • 思路分析

    1743382445270

  • 代码实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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
    """
    选择排序介绍:
    原理:
    每轮都假设该轮最前面的那个元素为最小值,然后去 剩下的列表中去寻找真正的最小值。
    最终交换即可。这样就找到了本轮的最小值。重复以上步骤即可。
    大白话:
    第一轮,假设i=0位置的元素是最小值。然后用min_index记住他索引,然后去剩下的所有元素中
    寻找最小值,找到了用min_index做记录。最终判断i和min_index是否交换。第一轮完毕后。
    最小值就在最小索引处。 重复该步骤,直至排序完成。
    分析流程:
    假设5个元素
    第几轮(索引) 该轮比较总次数 公式(具体的谁和谁比较)
    第一轮(0) 4次 索引0和1,2,3,4比较
    第二轮(1) 3次 索引1和2,3,4比较
    第三轮(2) 2次 索引2和3,4比较
    第四轮(3) 1次 索引3和4比较
    要点:
    1:比较的总轮数 列表长度-1
    2:每轮比较总次数 i+1 ~列表长度
    3:谁和谁比较? 索引min_index(初始i)和索引j比较,索引i和索引min_index的值交换
    """
    def select_sort(my_list):
    # 获取列表长度
    n = len(my_list)
    # 外层循环控制比较轮数
    for i in range(n - 1):
    # 定义变量min_index,记录本轮最小值的索引
    min_index = i
    # 内循环控制 每次比较轮数:次数
    for j in range(i + 1, n): # 1234 234 34 4
    # 具体的比较过程,索引min_index和索引j比较
    if my_list[j] < my_list[min_index]:
    min_index = j
    # 走到这里了。说明本轮已经找到最小值了,判断,并交换
    if min_index != i:
    my_list[min_index], my_list[i] = my_list[i], my_list[min_index]

    if __name__ == '__main__':
    my_list = [5, 3, 4, 7, 2]
    print(f"排序前:{my_list}")
    select_sort(my_list)
    print(f"排序后:{my_list}")


插入排序_思路及代码

1743382467741

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
"""
插入排序介绍:
原理:
把列表分为两个部分,假设第一个元素是有序的,剩下的是无序的。
每次都从无序的中获取一个元素。和有序前面的元素进行比较
决定出他的位置,进行插入。直至无序列表的元素操作完毕。剩下的列表就是:有序的
分析流程:
假设共有5个元素
第几轮 该轮数的比较总次数 公式(具体谁和谁比较)
第1轮 1次 索引1和索引0进行比较
第2轮 2次 索引2和索引1 ,索引2和索引0比较
第3轮 3次 索引3和2 索引3和1 索引3和0
第4轮 4次 索引4和3 索引4和2 索引4和1 索引4和0
要点:
1:比较总轮数 列表长度-1 range(1,n)
2:每轮比较的总次数 range(i,0,-1)
3:谁和谁比较 索引j和索引j-1位置的元素比较
最优:O(n)
最差:O(n²)
稳定 ??? 稳定排序
"""
def insert_sort(my_list):
n = len(my_list)
for i in range(1, n):
for j in range(i, 0, -1):
# 具体比较过程
if my_list[j] < my_list[j - 1]:
my_list[j], my_list[j - 1] = my_list[j - 1], my_list[j]
else:
# 走到这里说明元素已经找到了自己的位置
break
if __name__ == '__main__':
my_list = [5, 3, 4, 7, 2]
print(f"排序前:{my_list}")
insert_sort(my_list)
print(f"排序后:{my_list}")

二分查找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
"""
二分(折半)查找:
概述:
属于查找类的算法,相对来说效率比较高。时间复杂度 o(log n)
前提:
列表必须有序
原理:
1:比较 要查找元素和列表中的值,如果一样则返回True。程序结束
2:如果 要查找的元素比中值小,去前半段(中值前)查找。
3:如果 要查找的元素比中值大,去后半段(中值后)查找。
4:重复上述动作,直至找完。如果找完了,还找不到。就返回False
"""
# my_list 原列表 target要查找的目标值
def binary_search_re(my_list, target):
# 获取列表长度
n = len(my_list)
# 判断列表是否为空
if n == 0:
return False
mid = n // 2
# 比较 要查找的元素 和 中值
if my_list[mid] == target:
return True
elif target < my_list[mid]:
# 如果要查找的元素 比中值小 ,去前半段(中值前查找)。递归调用
# [:mid] 采用切片 ,包左不包右
return binary_search_re(my_list[:mid], target)
else:
return binary_search_re(my_list[mid + 1:], target)
return False


if __name__ == '__main__':
my_list = [2, 3, 5, 6, 8, 10, 16, 18, 20]
print(binary_search_re(my_list,60))

自定义代码模拟二叉树

  • 图解

    1743382494757

  • 代码框架

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    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
    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
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    # 定义Node类,表示二叉树节点
    class Node:
    # 初始化属性
    def __init__(self, item):
    self.item = item
    self.lchild = None
    self.rchild = None


    # 自定义BinaryTree ,表示二叉树
    class BinaryTree:
    def __init__(self, node=None):
    self.root = node

    # 定义add函数,表示添加节点
    def add(self, item):
    # 1.item封装为节点
    new_node = Node(item)
    # 2.判断根节点是否为null,如果为空,设置当前节点为跟节点
    if self.root is None:
    self.root = new_node
    return
    # 3.创建一个队列,添加根节点到队列中
    queue = []
    queue.append(self.root)
    # 4.通过while True 死循环 找到空缺的节点位置
    while True:
    # 5.获取队列第一个元素
    node = queue.pop(0)
    # 6.判断当前节点左子树是否为空
    if node.lchild is None:
    # 6.1把新节点当做当前节点的左子树,并结束
    node.lchild = new_node
    return
    else:
    # 6.2走到这里,说明左子树不为空,把当前节点的左子树添加队列中
    queue.append(node.lchild)

    # 7.判断当前节点的右子树是否为空
    if node.rchild is None:
    # 7.1把新节点设置为当前节点的右子树 ,并结束
    node.rchild = new_node
    return
    else:
    # 走到这里,说明右子树不为空,把当前节点的右子树,添加到队列中
    queue.append(node.rchild)

    # 定义遍历二叉树 广度优先遍历 (逐层遍历,一层一层的遍历)
    def breadth(self):
    # 1.判断根节点是否为空
    if self.root is None:
    return
    # 2.创建队列
    queue = []
    queue.append(self.root)
    # 3.循环打印内容
    while len(queue) != 0:
    # 4.获取第一个元素
    node = queue.pop(0)
    # 5.打印该节点的元素域
    print(node.item, end=" ")
    # 6.判断当前节点的左子树是否存在,存在添加到队列中
    if node.lchild is not None:
    queue.append(node.lchild)

    # 7.判断当前节点的右子树是否存在,存在添加到队列中
    if node.rchild is not None:
    queue.append(node.rchild)

    # 深度优先之先序遍历(根左右)
    def preorder(self, root):
    # 1.判断根节点是否不为空,不为空才打印
    if root is not None:
    print(root.item, end=" ")
    # 递归遍历左子树
    self.preorder(root.lchild)
    # 递归遍历右子树
    self.preorder(root.rchild)

    # 深度优先之中序遍历(左根右)
    def inorder(self, root):
    # 1.判断根节点是否不为空,不为空才打印
    if root is not None:
    # 递归遍历左子树
    self.inorder(root.lchild)
    print(root.item, end=" ")
    # 递归遍历右子树
    self.inorder(root.rchild)

    # 深度优先之后序遍历(左右根)
    def postorder(self,root):
    if root is not None:
    #遍历左子树
    self.postorder(root.lchild)
    #遍历右子树
    self.postorder(root.rchild)
    #打印根节点
    print(root.item,end=" ")


    def dm1_1_测试节点和二叉树类():
    # 1-创建节点
    node1 = Node("A")
    # 打印节点的元素域 左子树 右子树
    print(node1.item) # a
    print(node1.lchild) # None
    print(node1.rchild) # None

    print("--------二叉树类----------")
    # #创建了一个空树
    # bt = BinaryTree()
    # print(bt.root) #None

    print("--------测试二叉树----------")
    bt = BinaryTree(node1)
    print(bt.root) # <__main__.Node object at 0x000001E7F19438B0>
    print(bt.root.item)
    print(bt.root.lchild)
    print(bt.root.rchild)


    def dm2_2_测试添加节点():
    bt = BinaryTree()
    # 添加元素
    bt.add("A")
    bt.add("B")
    bt.add("C")
    bt.add("D")
    bt.add("E")
    bt.add("F")
    bt.add("G")
    bt.add("H")
    bt.add("I")
    bt.add("J")


    def dm3_3_测试添加_遍历节点():
    bt = BinaryTree()
    # 添加元素
    bt.add("A")
    bt.add("B")
    bt.add("C")
    bt.add("D")
    bt.add("E")
    bt.add("F")
    bt.add("G")
    bt.add("H")
    bt.add("I")
    bt.add("J")
    bt.breadth()


    def dm4_4_测试深度优先_先序遍历节点():
    bt = BinaryTree()
    # 添加元素
    bt.add("A")
    bt.add("B")
    bt.add("C")
    bt.add("D")
    bt.add("E")
    bt.add("F")
    bt.add("G")
    bt.add("H")
    bt.add("I")
    bt.add("J")
    print("先序遍历(根左右)")
    bt.preorder(bt.root)
    print()
    print("中序遍历(左根右)")
    bt.inorder(bt.root)
    print()
    print("后序遍历(左右根)")
    bt.postorder(bt.root)



    if __name__ == '__main__':
    # dm1_1_测试节点和二叉树类()
    # dm2_2_测试添加节点()
    # dm3_3_测试添加_遍历节点()
    dm4_4_测试深度优先_先序遍历节点()

    根据中序和先序_逆推二叉树结构

    1743382685040

线性回归

线性回归

image-20230912090316271

线性回归介绍

学习目标:

1.理解线性回归是什么?

2.知道一元线性回归和多元线性回归的区别

3.知道线性回归的应用场景

【理解】举个栗子

假若有了身高和体重数据,来了播仔的身高,你能预测播仔体重吗?

image-20230901101426794

这是一个回归问题,该如何求解呢?

思路:先从已知身高X和体重Y中找规律,再预测

image-20230901101621761

•数学问题:用一条线来拟合身高和体重之间的关系,再对新数据进行预测

image-20230901101918502

image-20230901101931661

方程 Y = kX + b

k160 + b = 56.3 – (1)

k166 + b = 60.6 –- (2)

。。。。

k: 斜率 b:截距

若:y = 0.9 x + (-93)

​ 0.9*176 +(-93)= ?

【理解】线性回归

线性回归(Linear regression)是利用 回归方程(函数)一个或多个自变量(特征值)和因变量(目标值)之间 关系进行建模的一种分析方式。

image-20230901102250602

image-20230901102402944

注意事项:

1 为什么叫线性模型?因为求解的w,都是w的零次幂(常数项)所以叫成线性模型

2 在线性回归中,从数据中获取的规律其实就是学习权重系数w

3 某一个权重值w越大,说明这个权重的数据对房子价格影响越大

【知道】线性回归分类

  • 一元线性回归

y = kx +b

目标值只与一个因变量有关系

image-20230901102857178

  • 多元线性回归

image-20230901102940614

目标值只与多个因变量有关系

image-20230901103000204

【知道】应用场景

image-20230901103123601

线性回归问题的求解

学习目标:

1.知道线性回归API的使用

2.知道损失函数是什么

3.复习导数和矩阵的相关内容

4.理解正规方程法

5.掌握梯度下降算法的内容

【实操】线性回归API的应用

预测播仔身高

已知数据:

image-20230901104147248

需求:播仔身高是176,请预测体重?

image-20230901104237860

通过线性回归API可快速的找到一条红色直线,是怎么求解的呢?

image-20230901104626001

【掌握】损失函数

需要设置一个评判标准

image-20230901110253313

误差概念:用预测值y – 真实值y就是误差

损失函数:衡量每个样本预测值与真实值效果的函数

“红色直线能更好的拟合所有点”也就是误差最小,误差和最小

损失函数数学如何表达呢?又如何求损失函数的最小值呢?

image-20230901110606206

image-20230901110842936

image-20230901110853094

当损失函数取最小值时,得到k就是最优解

image-20230901113810862

image-20230901113822728

image-20230901113834036

想求一条直线更好的拟合所有点 y = kx + b

  • ​ 引入损失函数(衡量预测值和真实值效果) Loss(k, b)

  • ​ 通过一个优化方法,求损失函数最小值,得到K最优解

image-20230912103729099

回归的损失函数:

  • 均方误差 (Mean-Square Error, MSE)

image-20230901114527268

  • 平均绝对误差 (Mean Absolute Error* , *MAE)

    image-20230901114539398

image-20230901114816872

【复习】导数和矩阵

【知道】常见的数据表述

  • 为什么要学习标量、向量、矩阵、张量?
    • 因机器学习、深度学习中经常用,不要因是数学就害怕
    • 宗旨:用到就学什么,不要盲目的展开、大篇幅学数学
  • 标量scalar :一个独立存在的数,只有大小没有方向
  • 向量vector :向量指一列顺序排列的元素。默认是列向量

image-20230901115201758

image-20230901115210623

  • 矩阵matrix :二维数组

    image-20230901115223778

    image-20230901115232323

  • 张量Tensor :多维数组,张量是基于向量和矩阵的推广

    image-20230901115246275

【知道】导数

当函数y=f(x)的自变量x在一点$x_0$上产生一个增量Δx时,函数输出值的增量Δy与自变量增量Δx的比值在Δx趋于0时的极限a如果存在,a即为在$x_0$处的导数,记作$f^\prime(x_0)$或df($x_0$)/dx。

导数是函数的局部性质。一个函数在某一点的导数描述了这个函数在这一点附近的变化率。

函数在某一点的导数就是该函数所代表的曲线在这一点上的切线斜率

常见函数的导数:

公式 例子
$(C)^\prime=0$ $\left(5\right)^\prime=0$ $\left(10\right)^\prime=0$
$\left(x^\alpha\right)^\prime=\alpha x^{\alpha-1}$ $\left(x^3\right)^\prime=3 x^{2}$ $\left(x^5\right)^\prime=5 x^{4}$
$\left(a^x\right)^\prime=a^{x}\ln{a}$ $\left(2^x\right)^\prime=2^x\ln{2}$ $\left(7^x\right)^\prime=7^x\ln{7}$
$\left(e^x\right)^\prime=e^{x}$ $\left(e^x\right)^\prime=e^{x}$
$\left(\log{_a}x\right)^\prime=\frac{1}{x\ln{a}}$ $\left(\log{{10}}x\right)^\prime=\frac{1}{x\ln{10}}$ $\left(\log{{6}}x\right)^\prime=\frac{1}{x\ln{6}}$
$\left(\ln{x}\right)^\prime=\frac{1}{x}$ $\left(\ln{x}\right)^\prime=\frac{1}{x}$
$\left(\sin{x}\right)^\prime=\cos{x}$ $\left(\sin{x}\right)^\prime=\cos{x}$
$\left(\cos{x}\right)^\prime=-\sin{x}$ $\left(\cos{x}\right)^\prime=-\sin{x}$

导数的四则运算:

公式 例子
$\left[u(x)\pm v(x)\right]^\prime=u^\prime(x) \pm v^\prime(x)$ $(e^x+4\ln{x})^\prime=(e^x)^\prime+(4\ln{x})^\prime=e^x+\frac{4}{x}$
$\left[u(x)\cdot v(x)\right]^\prime=u^\prime(x) \cdot v(x) + u(x) \cdot v^\prime(x)$ $(\sin{x}\cdot\ln{x})^\prime=\cos{x}\cdot\ln{x}+\sin{x}\cdot\frac{1}{x}$
$\left[\frac{u(x)}{v(x)}\right]^\prime=\frac{u^\prime(x) \cdot v(x) - u(x) \cdot v^\prime(x)}{v^2(x)}$ $\left(\frac{e^x}{\cos{x}}\right)^\prime=\frac{e^x\cdot\cos{x}-e^x\cdot(-\sin{x})}{cos^2(x)}$
${g[h(x)]}^\prime=g^\prime(h)*h^\prime(x)$ $(\sin{2x})^\prime=\cos{2x}\cdot(2x)^\prime=2\cos(2x)$

复合函数求导:g(h)是外函数 h(x)是内函数。先对外函数求导,再对内函数求导

image-20230901143204713

导数求极值:

导数为0的位置是函数的极值点

image-20230901143529603

【知道】偏导

image-20230901144053678

image-20230901144102962

【知道】向量

向量运算:

image-20230901144711751

image-20230901145009232

【知道】矩阵

image-20230901145231388

image-20230901145322438

image-20230901145840373

image-20230901150113602

【了解】一元线性回归的解析解

image-20230901150406897

image-20230901150708528

image-20230912145402703

【了解】多元线性回归的解析解-正规方程法

image-20230901152528805

image-20230911234116911

image-20230901152745308

image-20230901152758396

梯度下降算法

【掌握】梯度下降算法思想

什么是梯度下降法

• 求解函数极值还有更通用的方法就是梯度下降法。顾名思义:沿着梯度下降的方向求解极小值 • 举个例子:坡度最陡下山法

image-20230901183007785

  • 输入:初始化位置S;每步距离为a 。输出:从位置S到达山底
  • 步骤1:令初始化位置为山的任意位置S
  • 步骤2:在当前位置环顾四周,如果四周都比S高返回S;否则执行步骤3
  • 步骤3: 在当前位置环顾四周,寻找坡度最陡的方向,令其为x方向
  • 步骤4:沿着x方向往下走,长度为a,到达新的位置S‘
  • 步骤5:在S‘位置环顾四周,如果四周都比S‘高,则返回S‘。否则转到步骤3

小结:通过循环迭代的方法不断更新位置S (相当于不断更新权重参数w)

image-20230901183020726

最终找到最优解 这个方法可用来求损失函数最优解, 比正规方程更通用

1
2
3
梯度下降过程就和下山场景类似
可微分的损失函数,代表着一座山
寻找的函数的最小值,也就是山底

image-20230901183113930

image-20230901183125661

image-20230912163031832

image-20230901183139728

image-20230901183152966

【理解】银行信贷案例

image-20230901183222050

image-20230901183240178

image-20230901183301618

image-20230901183315009

【理解】正规方程和梯度下降算法的对比

image-20230901162716376

回归评估方法

学习目标:

1.掌握常用的回归评估方法

2.了解不同评估方法的特点

为什么要进行线性回归模型的评估

我们希望衡量预测值和真实值之间的差距,

会用到MAE、MSE、RMSE多种测评函数进行评价

【知道】平均绝对误差

Mean Absolute Error (MAE)

img

  • 上面的公式中:n 为样本数量, y 为实际值, $\hat{y}$ 为预测值

  • MAE 越小模型预测约准确

Sklearn 中MAE的API

1
2
from sklearn.metrics import mean_absolute_error
mean_absolute_error(y_test,y_predict)

【知道】均方误差

Mean Squared Error (MSE)

img

  • 上面的公式中:n 为样本数量, y 为实际值, $\hat{y}$ 为预测值
  • MSE 越小模型预测约准确

Sklearn 中MSE的API

1
2
from sklearn.metrics import mean_squared_error
mean_squared_error(y_test,y_predict)

【知道】 均方根误差

Root Mean Squared Error (RMSE)

img

  • 上面的公式中:n 为样本数量, y 为实际值, $\hat{y}$ 为预测值
  • RMSE 越小模型预测约准确

【了解】 三种指标的比较

我们绘制了一条直线 y = 2x +5 用来拟合 y = 2x + 5 + e. 这些数据点,其中e为噪声

img

从上图中我们发现 MAE 和 RMSE 非常接近,都表明模型的误差很低(MAE 或 RMSE 越小,误差越小!)。 但是MAE 和 RMSE 有什么区别?为什么MAE较低?

  • 对比MAE 和 RMSE的公式,RMSE的计算公式中有一个平方项,因此:大的误差将被平方,因此会增加 RMSE 的值

  • 可以得出结论,RMSE 会放大预测误差较大的样本对结果的影响,而 MAE 只是给出了平均误差

  • 由于 RMSE 对误差的 平方和求平均 再开根号,大多数情况下RMSE>MAE

    举例 (1+3)/2 = 2 $\sqrt{(1^2+3^2)/2 }= \sqrt{10/2} = \sqrt{5} = 2.236$

我们再看下一个例子

img

橙色线与第一张图中的直线一样:y = 2x +5

蓝色的点为: y = y + sin(x)*exp(x/20) + e 其中 exp() 表示指数函数

我们看到对比第一张图,所有的指标都变大了,RMSE 几乎是 MAE 值的两倍,因为它对预测误差较大的点比较敏感

我们是否可以得出结论: RMSE是更好的指标? 某些情况下MAE更有优势,例如:

  • 假设数据中有少数异常点偏差很大,如果此时根据 RMSE 选择线性回归模型,可能会选出过拟合的模型来
  • 在这种情况下,由于数据中的异常点极少,选择具有最低 MAE 的回归模型可能更合适
  • 除此之外,当两个模型计算RMSE时数据量不一致,也不适合在一起比较

【实操】波士顿房价预测案例

【知道】线性回归API

sklearn.linear_model.LinearRegression(fit_intercept=True)

  • 通过正规方程优化
  • 参数:fit_intercept,是否计算偏置
  • 属性:LinearRegression.coef_ (回归系数) LinearRegression.intercept_(偏置)

sklearn.linear_model.SGDRegressor(loss=”squared_loss”, fit_intercept=True, learning_rate =’constant’, eta0=0.01)

  • 参数:loss(损失函数类型),fit_intercept(是否计算偏置)learning_rate (学习率)
  • 属性:SGDRegressor.coef_ (回归系数)SGDRegressor.intercept_ (偏置)

【实操】波士顿房价预测

image-20230913092037241

案例背景介绍

数据介绍

属性

给定的这些特征,是专家们得出的影响房价的结果属性。我们此阶段不需要自己去探究特征是否有用,只需要使用这些特征。到后面量化很多特征需要我们自己去寻找

案例分析

回归当中的数据大小不一致,是否会导致结果影响较大。所以需要做标准化处理。

  • 数据分割与标准化处理
  • 回归预测
  • 线性回归的算法效果评估

回归性能评估

均方误差(Mean Squared Error, MSE)评价机制:

$\Large MSE = \frac{1}{m}\sum_{i=1}^{m}(y^i-\hat{y})^2$

sklearn中的API:sklearn.metrics.mean_squared_error(y_true, y_pred)

  • 均方误差回归损失
  • y_true:真实值
  • y_pred:预测值
  • return:浮点数结果

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
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
# 0.导包
from sklearn.datasets import load_boston
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LinearRegression,SGDRegressor
from sklearn.metrics import mean_squared_error

# 1.加载数据
boston = load_boston()
# print(boston)

# 2.数据集划分
x_train,x_test,y_train,y_test =train_test_split(boston.data,boston.target,test_size=0.2,random_state=22)

# 3.标准化
process=StandardScaler()
x_train=process.fit_transform(x_train)
x_test=process.transform(x_test)

# 4.模型训练
# 4.1 实例化(正规方程)
# model =LinearRegression(fit_intercept=True)
model = SGDRegressor(learning_rate='constant',eta0=0.01)
# 4.2 fit
model.fit(x_train,y_train)

# print(model.coef_)
# print(model.intercept_)
# 5.预测
y_predict=model.predict(x_test)

print(y_predict)

# 6.模型评估

print(mean_squared_error(y_test,y_predict))


1.2.0 以上版本实现

1
2
3
4
5
6
7
8
9
10
11
12
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
# 0.导包
# from sklearn.datasets import load_boston
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LinearRegression,SGDRegressor
from sklearn.metrics import mean_squared_error

# 1.加载数据
# boston = load_boston()
# print(boston)
import pandas as pd
import numpy as np


data_url = "http://lib.stat.cmu.edu/datasets/boston"
raw_df = pd.read_csv(data_url, sep="\s+", skiprows=22, header=None)
data = np.hstack([raw_df.values[::2, :], raw_df.values[1::2, :2]])
target = raw_df.values[1::2, 2]

# 2.数据集划分
# x_train,x_test,y_train,y_test =train_test_split(boston.data,boston.target,test_size=0.2,random_state=22)
x_train,x_test,y_train,y_test =train_test_split(data,target,test_size=0.2,random_state=22)

# 3.标准化
process=StandardScaler()
x_train=process.fit_transform(x_train)
x_test=process.transform(x_test)

# 4.模型训练
# 4.1 实例化(正规方程)
# model =LinearRegression(fit_intercept=True)
model = SGDRegressor(learning_rate='constant',eta0=0.01)
# 4.2 fit
model.fit(x_train,y_train)

# print(model.coef_)
# print(model.intercept_)
# 5.预测
y_predict=model.predict(x_test)

print(y_predict)

# 6.模型评估

print(mean_squared_error(y_test,y_predict))


正则化

学习目标:

1.掌握过拟合、欠拟合的概念

2.掌握过拟合、欠拟合产生的原因

3.知道什么是正则化,以及正则化的方法

【理解】 欠拟合与过拟合

过拟合:一个假设 在训练数据上能够获得比其他假设更好的拟合, 但是在测试数据集上却不能很好地拟合数据 (体现在准确率下降),此时认为这个假设出现了过拟合的现象。(模型过于复杂)

欠拟合:一个假设 在训练数据上不能获得更好的拟合,并且在测试数据集上也不能很好地拟合数据 ,此时认为这个假设出现了欠拟合的现象。(模型过于简单)

过拟合和欠拟合的区别:

æ¬ æ‹Ÿåˆè¿‡æ‹Ÿåˆå›¾ç¤º

欠拟合在训练集和测试集上的误差都较大

过拟合在训练集上误差较小,而测试集上误差较大

image-20230913101352444

【实践】通过代码认识过拟合和欠拟合

绘制数据

1
2
3
4
5
6
7
8
9
10
11
12
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
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error # 计算均方误差
from sklearn.model_selection import train_test_split


def dm01_欠拟合():
# 1. 准备x, y数据, 增加上噪声.
# 用于设置随机数生成器的种子(seed), 种子一样, 每次生成相同序列.
np.random.seed(666)
# x: 随机数, 范围为 (-3, 3), 100个.
x = np.random.uniform(-3, 3, size=100)
# loc: 均值, scale: 标准差, normal: 正态分布.
y = 0.5 * x ** 2 + x + 2 + np.random.normal(0, 1, size=100)
# 2. 实例化 线性回归模型.
estimator = LinearRegression()
# 3. 训练模型
X = x.reshape(-1, 1)
estimator.fit(X, y)

# 4. 模型预测.
y_predict = estimator.predict(X)
print("预测值:", y_predict)

# 5. 计算均方误差 => 模型评估
print(f'均方误差: {mean_squared_error(y, y_predict)}')
# 6. 画图
plt.scatter(x, y) # 散点图
plt.plot(x, y_predict, color='r') # 折线图(预测值, 拟合回归线)
plt.show() # 具体的绘图



if __name__ == '__main__':
dm01_欠拟合()

1

1
2
3
4
5
#计算均方误差
from sklearn.metrics import mean_squared_error
mean_squared_error(y,y_predict)

#3.0750025765636577

添加二次项,绘制图像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def dm02_模型ok():
# 1. 准备x, y数据, 增加上噪声.
# 用于设置随机数生成器的种子(seed), 种子一样, 每次生成相同序列.
np.random.seed(666)
# x: 随机数, 范围为 (-3, 3), 100个.
x = np.random.uniform(-3, 3, size=100)
# loc: 均值, scale: 标准差, normal: 正态分布.
y = 0.5 * x ** 2 + x + 2 + np.random.normal(0, 1, size=100)
# 2. 实例化 线性回归模型.
estimator = LinearRegression()
# 3. 训练模型
X = x.reshape(-1, 1)
X2 = np.hstack([X, X ** 2])
estimator.fit(X2, y)

# 4. 模型预测.
y_predict = estimator.predict(X2)
print("预测值:", y_predict)

# 5. 计算均方误差 => 模型评估
print(f'均方误差: {mean_squared_error(y, y_predict)}')
# 6. 画图
plt.scatter(x, y) # 散点图
# sort() 该函数直接返回一个排序后的新数组。
# numpy.argsort() 该函数返回的是数组值从小到大排序时对应的索引值
plt.plot(np.sort(x), y_predict[np.argsort(x)], color='r') # 折线图(预测值, 拟合回归线)
# plt.plot(x, y_predict)
plt.show() # 具体的绘图

2

1
2
3
4
5
6
#计算均方误差和准确率

from sklearn.metrics import mean_squared_error
mean_squared_error(y,y_predict2)

#1.0987392142417858

再次加入高次项,绘制图像,观察均方误差结果

1
2
3
4
5
6
7
8
9
10
11
12
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
56
57
58
59
60
61
62
63
64
65
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
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error # 计算均方误差
from sklearn.model_selection import train_test_split


def dm01_欠拟合():
# 1. 准备x, y数据, 增加上噪声.
# 用于设置随机数生成器的种子(seed), 种子一样, 每次生成相同序列.
np.random.seed(666)
# x: 随机数, 范围为 (-3, 3), 100个.
x = np.random.uniform(-3, 3, size=100)
# loc: 均值, scale: 标准差, normal: 正态分布.
y = 0.5 * x ** 2 + x + 2 + np.random.normal(0, 1, size=100)
# 2. 实例化 线性回归模型.
estimator = LinearRegression()
# 3. 训练模型
X = x.reshape(-1, 1)
estimator.fit(X, y)

# 4. 模型预测.
y_predict = estimator.predict(X)
print("预测值:", y_predict)

# 5. 计算均方误差 => 模型评估
print(f'均方误差: {mean_squared_error(y, y_predict)}')
# 6. 画图
plt.scatter(x, y) # 散点图
plt.plot(x, y_predict, color='r') # 折线图(预测值, 拟合回归线)
plt.show() # 具体的绘图

def dm02_模型ok():
# 1. 准备x, y数据, 增加上噪声.
# 用于设置随机数生成器的种子(seed), 种子一样, 每次生成相同序列.
np.random.seed(666)
# x: 随机数, 范围为 (-3, 3), 100个.
x = np.random.uniform(-3, 3, size=100)
# loc: 均值, scale: 标准差, normal: 正态分布.
y = 0.5 * x ** 2 + x + 2 + np.random.normal(0, 1, size=100)
# 2. 实例化 线性回归模型.
estimator = LinearRegression()
# 3. 训练模型
X = x.reshape(-1, 1)
X2 = np.hstack([X, X ** 2])
estimator.fit(X2, y)

# 4. 模型预测.
y_predict = estimator.predict(X2)
print("预测值:", y_predict)

# 5. 计算均方误差 => 模型评估
print(f'均方误差: {mean_squared_error(y, y_predict)}')
# 6. 画图
plt.scatter(x, y) # 散点图
# sort() 该函数直接返回一个排序后的新数组。
# numpy.argsort() 该函数返回的是数组值从小到大排序时对应的索引值
plt.plot(np.sort(x), y_predict[np.argsort(x)], color='r') # 折线图(预测值, 拟合回归线)
# plt.plot(x, y_predict)
plt.show() # 具体的绘图


def dm03_过拟合():
# 1. 准备x, y数据, 增加上噪声.
# 用于设置随机数生成器的种子(seed), 种子一样, 每次生成相同序列.
np.random.seed(666)
# x: 随机数, 范围为 (-3, 3), 100个.
x = np.random.uniform(-3, 3, size=100)
# loc: 均值, scale: 标准差, normal: 正态分布.
y = 0.5 * x ** 2 + x + 2 + np.random.normal(0, 1, size=100)
# 2. 实例化 线性回归模型.
estimator = LinearRegression()
# 3. 训练模型
X = x.reshape(-1, 1)
# hstack() 函数用于将多个数组在行上堆叠起来, 即: 数据增加高次项.
X3 = np.hstack([X, X**2, X**3, X**4, X**5, X**6, X**7, X**8, X**9, X**10])
estimator.fit(X3, y)

# 4. 模型预测.
y_predict = estimator.predict(X3)
print("预测值:", y_predict)

# 5. 计算均方误差 => 模型评估
print(f'均方误差: {mean_squared_error(y, y_predict)}')
# 6. 画图
plt.scatter(x, y) # 散点图
# sort() 该函数直接返回一个排序后的新数组。
# numpy.argsort() 该函数返回的是数组值从小到大排序时对应的索引值
plt.plot(np.sort(x), y_predict[np.argsort(x)], color='r') # 折线图(预测值, 拟合回归线)
plt.show() # 具体的绘图

if __name__ == '__main__':
# dm01_欠拟合()
# dm02_模型ok()
dm03_过拟合()

通过上述观察发现,随着加入的高次项越来越多,拟合程度越来越高,均方误差也随着加入越来越小。说明已经不再欠拟合了。

问题:如何判断出现过拟合呢?

将数据集进行划分:对比X、X2、X5的测试集的均方误差

X的测试集均方误差

1
2
3
4
5
6
7
X_train,X_test,y_train,y_test = train_test_split(X,y,random_state = 5)
estimator = LinearRegression()
estimator.fit(X_train,y_train)
y_predict = estimator.predict(X_test)

mean_squared_error(y_test,y_predict)
#3.153139806483088

X2的测试集均方误差

1
2
3
4
5
6
X_train,X_test,y_train,y_test = train_test_split(X2,y,random_state = 5)
estimator = LinearRegression()
estimator.fit(X_train,y_train)
y_predict = estimator.predict(X_test)
mean_squared_error(y_test,y_predict)
#1.111873885731967

X5的测试集的均方误差

1
2
3
4
5
6
X_train,X_test,y_train,y_test = train_test_split(X5,y,random_state = 5)
estimator = LinearRegression()
estimator.fit(X_train,y_train)
y_predict = estimator.predict(X_test)
mean_squared_error(y_test,y_predict)
#1.4145580542309835

【理解】 原因以及解决办法

欠拟合产生原因: 学习到数据的特征过少

解决办法:

1)添加其他特征项,有时出现欠拟合是因为特征项不够导致的,可以添加其他特征项来解决

2)添加多项式特征,模型过于简单时的常用套路,例如将线性模型通过添加二次项或三次项使模型泛化能力更强

过拟合产生原因: 原始特征过多,存在一些嘈杂特征, 模型过于复杂是因为模型尝试去兼顾所有测试样本

解决办法:

1)重新清洗数据,导致过拟合的一个原因有可能是数据不纯,如果出现了过拟合就需要重新清洗数据。

2)增大数据的训练量,还有一个原因就是我们用于训练的数据量太小导致的,训练数据占总数据的比例过小。

3)正则化

4)减少特征维度

【理解】正则化

在解决回归过拟合中,我们选择正则化。但是对于其他机器学习算法如分类算法来说也会出现这样的问题,除了一些算法本身作用之外(决策树、神经网络),我们更多的也是去自己做特征选择,包括之前说的删除、合并一些特征

如何解决?

正则化

在学习的时候,数据提供的特征有些影响模型复杂度或者这个特征的数据点异常较多,所以算法在学习的时候尽量减少这个特征的影响(甚至删除某个特征的影响),这就是正则化

注:调整时候,算法并不知道某个特征影响,而是去调整参数得出优化的结

L1正则化

  • 假设𝐿(𝑊)是未加正则项的损失,𝜆是一个超参,控制正则化项的大小。
  • 则最终的损失函数:$𝐿=𝐿(𝑊)+ \lambda*\sum_{i=1}^{n}\lvert w_i\rvert$

作用:用来进行特征选择,主要原因在于L1正则化会使得较多的参数为0,从而产生稀疏解,可以将0对应的特征遗弃,进而用来选择特征。一定程度上L1正则也可以防止模型过拟合。

image-20230913145042318

L1正则为什么可以产生稀疏解(可以特征选择)

稀疏性:向量中很多维度值为0

  • 对其中的一个参数 $ w_i $ 计算梯度,其他参数同理,α是学习率,sign(wi)是符号函数。
l2

L1的梯度:

$𝐿=𝐿(𝑊)+ \lambda*\sum_{i=1}^{n}\lvert w_i\rvert$

$\frac{\partial L}{\partial w_{i}} = \frac{\partial L(W)}{\partial w_{i}}+\lambda sign(w_{i})$

LASSO回归: from sklearn.linear_model import Lasso

L2正则化

  • 假设𝐿(𝑊)是未加正则项的损失,𝜆是一个超参,控制正则化项的大小。
  • 则最终的损失函数:$𝐿=𝐿(𝑊)+ \lambda*\sum_{i=1}^{n}w_{i}^{2}$

作用:主要用来防止模型过拟合,可以减小特征的权重

优点:越小的参数说明模型越简单,越简单的模型则越不容易产生过拟合现象

Ridge回归: from sklearn.linear_model import Ridge

正则化案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
X10 = np.hstack([X2,X**3,X**4,X**5,X**6,X**7,X**8,X**9,X**10]) 
estimator3 = LinearRegression()
estimator3.fit(X10,y)
y_predict3 = estimator3.predict(X10)

plt.scatter(x,y)
plt.plot(np.sort(x),y_predict3[np.argsort(x)],color = 'r')
plt.show()

estimator3.coef_

array([ 1.32292089e+00, 2.03952017e+00, -2.88731664e-01, -1.24760429e+00,
8.06147066e-02, 3.72878513e-01, -7.75395040e-03, -4.64121137e-02,
1.84873446e-04, 2.03845917e-03])

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from sklearn.linear_model import Lasso  # L1正则
from sklearn.linear_model import Ridge # 岭回归 L2正则

def dm04_模型过拟合_L1正则化():
# 1. 准备x, y数据, 增加上噪声.
# 用于设置随机数生成器的种子(seed), 种子一样, 每次生成相同序列.
np.random.seed(666)
# x: 随机数, 范围为 (-3, 3), 100个.
x = np.random.uniform(-3, 3, size=100)
# loc: 均值, scale: 标准差, normal: 正态分布.
y = 0.5 * x ** 2 + x + 2 + np.random.normal(0, 1, size=100)
# 2. 实例化L1正则化模型, 做实验: alpha惩罚力度越来越大, k值越来越小.
estimator = Lasso(alpha=0.005)
# 3. 训练模型
X = x.reshape(-1, 1)
# hstack() 函数用于将多个数组在行上堆叠起来, 即: 数据增加高次项.
X3 = np.hstack([X, X**2, X**3, X**4, X**5, X**6, X**7, X**8, X**9, X**10])
estimator.fit(X3, y)
print(f'权重: {estimator.coef_}')

# 4. 模型预测.
y_predict = estimator.predict(X3)
print("预测值:", y_predict)

# 5. 计算均方误差 => 模型评估
print(f'均方误差: {mean_squared_error(y, y_predict)}')
# 6. 画图
plt.scatter(x, y) # 散点图
# sort() 该函数直接返回一个排序后的新数组。
# numpy.argsort() 该函数返回的是数组值从小到大排序时对应的索引值
plt.plot(np.sort(x), y_predict[np.argsort(x)], color='r') # 折线图(预测值, 拟合回归线)
plt.show() # 具体的绘图

img

1
2
3
4
5
6
7
8
9
10
11
12
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
56
57
58
59
60
61
62
63
64
65
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
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
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression, Lasso, Ridge
from sklearn.metrics import mean_squared_error # 计算均方误差
from sklearn.model_selection import train_test_split


def dm01_欠拟合():
# 1. 准备x, y数据, 增加上噪声.
# 用于设置随机数生成器的种子(seed), 种子一样, 每次生成相同序列.
np.random.seed(666)
# x: 随机数, 范围为 (-3, 3), 100个.
x = np.random.uniform(-3, 3, size=100)
# loc: 均值, scale: 标准差, normal: 正态分布.
y = 0.5 * x ** 2 + x + 2 + np.random.normal(0, 1, size=100)
# 2. 实例化 线性回归模型.
estimator = LinearRegression()
# 3. 训练模型
X = x.reshape(-1, 1)
estimator.fit(X, y)

# 4. 模型预测.
y_predict = estimator.predict(X)
print("预测值:", y_predict)

# 5. 计算均方误差 => 模型评估
print(f'均方误差: {mean_squared_error(y, y_predict)}')
# 6. 画图
plt.scatter(x, y) # 散点图
plt.plot(x, y_predict, color='r') # 折线图(预测值, 拟合回归线)
plt.show() # 具体的绘图

def dm02_模型ok():
# 1. 准备x, y数据, 增加上噪声.
# 用于设置随机数生成器的种子(seed), 种子一样, 每次生成相同序列.
np.random.seed(666)
# x: 随机数, 范围为 (-3, 3), 100个.
x = np.random.uniform(-3, 3, size=100)
# loc: 均值, scale: 标准差, normal: 正态分布.
y = 0.5 * x ** 2 + x + 2 + np.random.normal(0, 1, size=100)
# 2. 实例化 线性回归模型.
estimator = LinearRegression()
# 3. 训练模型
X = x.reshape(-1, 1)
X2 = np.hstack([X, X ** 2])
estimator.fit(X2, y)

# 4. 模型预测.
y_predict = estimator.predict(X2)
print("预测值:", y_predict)

# 5. 计算均方误差 => 模型评估
print(f'均方误差: {mean_squared_error(y, y_predict)}')
# 6. 画图
plt.scatter(x, y) # 散点图
# sort() 该函数直接返回一个排序后的新数组。
# numpy.argsort() 该函数返回的是数组值从小到大排序时对应的索引值
plt.plot(np.sort(x), y_predict[np.argsort(x)], color='r') # 折线图(预测值, 拟合回归线)
# plt.plot(x, y_predict)
plt.show() # 具体的绘图

def dm03_过拟合():
# 1. 准备x, y数据, 增加上噪声.
# 用于设置随机数生成器的种子(seed), 种子一样, 每次生成相同序列.
np.random.seed(666)
# x: 随机数, 范围为 (-3, 3), 100个.
x = np.random.uniform(-3, 3, size=100)
# loc: 均值, scale: 标准差, normal: 正态分布.
y = 0.5 * x ** 2 + x + 2 + np.random.normal(0, 1, size=100)
# 2. 实例化 线性回归模型.
estimator = LinearRegression()
# 3. 训练模型
X = x.reshape(-1, 1)
# hstack() 函数用于将多个数组在行上堆叠起来, 即: 数据增加高次项.
X3 = np.hstack([X, X**2, X**3, X**4, X**5, X**6, X**7, X**8, X**9, X**10])
estimator.fit(X3, y)

# 4. 模型预测.
y_predict = estimator.predict(X3)
print("预测值:", y_predict)

# 5. 计算均方误差 => 模型评估
print(f'均方误差: {mean_squared_error(y, y_predict)}')
# 6. 画图
plt.scatter(x, y) # 散点图
# sort() 该函数直接返回一个排序后的新数组。
# numpy.argsort() 该函数返回的是数组值从小到大排序时对应的索引值
plt.plot(np.sort(x), y_predict[np.argsort(x)], color='r') # 折线图(预测值, 拟合回归线)
plt.show() # 具体的绘图


def dm04_模型过拟合_L1正则化():
# 1. 准备x, y数据, 增加上噪声.
# 用于设置随机数生成器的种子(seed), 种子一样, 每次生成相同序列.
np.random.seed(666)
# x: 随机数, 范围为 (-3, 3), 100个.
x = np.random.uniform(-3, 3, size=100)
# loc: 均值, scale: 标准差, normal: 正态分布.
y = 0.5 * x ** 2 + x + 2 + np.random.normal(0, 1, size=100)
# 2. 实例化L1正则化模型, 做实验: alpha惩罚力度越来越大, k值越来越小.
estimator = Lasso(alpha=0.005)
# 3. 训练模型
X = x.reshape(-1, 1)
# hstack() 函数用于将多个数组在行上堆叠起来, 即: 数据增加高次项.
X3 = np.hstack([X, X**2, X**3, X**4, X**5, X**6, X**7, X**8, X**9, X**10])
estimator.fit(X3, y)
print(f'权重: {estimator.coef_}')

# 4. 模型预测.
y_predict = estimator.predict(X3)
print("预测值:", y_predict)

# 5. 计算均方误差 => 模型评估
print(f'均方误差: {mean_squared_error(y, y_predict)}')
# 6. 画图
plt.scatter(x, y) # 散点图
# sort() 该函数直接返回一个排序后的新数组。
# numpy.argsort() 该函数返回的是数组值从小到大排序时对应的索引值
plt.plot(np.sort(x), y_predict[np.argsort(x)], color='r') # 折线图(预测值, 拟合回归线)
plt.show() # 具体的绘图

def dm05_模型过拟合_L2正则化():
# 1. 准备x, y数据, 增加上噪声.
# 用于设置随机数生成器的种子(seed), 种子一样, 每次生成相同序列.
np.random.seed(666)
# x: 随机数, 范围为 (-3, 3), 100个.
x = np.random.uniform(-3, 3, size=100)
# loc: 均值, scale: 标准差, normal: 正态分布.
y = 0.5 * x ** 2 + x + 2 + np.random.normal(0, 1, size=100)
# 2. 实例化L2正则化模型, 做实验: alpha惩罚力度越来越大, k值越来越小.
estimator = Ridge(alpha=0.005)
# 3. 训练模型
X = x.reshape(-1, 1)
# hstack() 函数用于将多个数组在行上堆叠起来, 即: 数据增加高次项.
X3 = np.hstack([X, X**2, X**3, X**4, X**5, X**6, X**7, X**8, X**9, X**10])
estimator.fit(X3, y)
print(f'权重: {estimator.coef_}')

# 4. 模型预测.
y_predict = estimator.predict(X3)
print("预测值:", y_predict)

# 5. 计算均方误差 => 模型评估
print(f'均方误差: {mean_squared_error(y, y_predict)}')
# 6. 画图
plt.scatter(x, y) # 散点图
# sort() 该函数直接返回一个排序后的新数组。
# numpy.argsort() 该函数返回的是数组值从小到大排序时对应的索引值
plt.plot(np.sort(x), y_predict[np.argsort(x)], color='r') # 折线图(预测值, 拟合回归线)
plt.show() # 具体的绘图

if __name__ == '__main__':
# dm01_欠拟合()
# dm02_模型ok()
dm03_过拟合()
# dm04_模型过拟合_L1正则化()
# dm05_模型过拟合_L2正则化()

img

作业

1.完成线性回归部分的思维导图

2.描述梯度下降算法思想,自己推导银行信贷的案例

3.说明欠拟合和过拟合的相关内容

3.使用L1和L2正则化方法实现波士顿房价预测